Selam bu yazımda Go programlama dili ile web uygulaması yazmasını inceliyorum.
Bu yazıda şunları görecekmişiz :
- Kaydetme ve okuma metodlarıyla beraber bir veri yapısı (struct) tanımlamak
- net/http paketini kullanarak bir web uygulaması yapmak
- html/template paketi kullanarak HTML şablonları işlemek
- regexp paketini kullanarak kullanıcının girdiği bilgiyi doğrulamak
- closure kullanmak
Başlamadan önce olması gerekenler:
- Programlama bilgisi
- Temel web teknolojilerinden anlamak (Http, Html)
- Unix/Dos komut satırı kullanmak
İlk Adım
Bilgisayarınızda Go programlama dili yüklü ve çalışıyor olmalıdır. Komut isteminde (terminalde) girilen komutları $ işareti ile başlayan satırlarla göstereceğiz.
Uygulamamız için yeni bir klasör oluşturup onun içine girelim.
Klasörümüzde wiki.go adında yeni bir dosya oluşturup favori kod editörümüzde açalım ve içine şu kodu yazalım :
Şimdilik Go standart kütüphanelerinden fmt ve os kütüphanelerini programa dahil (import) ettik. İlerde başka fonksiyonlara ihtiyaç duydukça bu import bloğuna ekleyeceğiz.
Veri Yapılarımız
Veri yapılarını tanımlayarak başlayalım. Bir wiki birbirine bağlı sayfalardan oluşur. Bunların her birinde bir başlık (title) ve bir gövde metni (body- içerik) bulunur. Page adında bir struct tanımlıyoruz. 2 alt alanı var, Title ve Body.
Veri tipi []byte demek bir byte slice demek. Body elemanı veri tipinde bir string yerine []byte kullanıldı, çünkü bu veri tipi aşağıda göreceğimiz üzere io fonksiyonları için daha uygun olur.
Page struct değişkeni tanımı sayfa bilgilerinin hafızada nasıl saklanacağını belirliyor. Fakat bu verileri kalıcı bir hafızada saklamamız gerekiyor. Page yapısının verilerini saklamak için bir save fonksiyonu tanımlıyoruz.
Bu metod tanımlamasını şöyle açıklayabiliriz: Bu metod alıcısı (bağlandığı değişken) p olan (Page tipi bir veriye pointer) save adlı bir metod. Dönen değeri ise error tipinde ve parametresi yok. Bu şekilde alıcısı olan metod tanımladığımız için bu metodu artık Page tipi değerler için kullanabiliriz.
Bu metod Page'in Body elemanı değerini bir text dosyaya saklıyor. Basit olsun diye Title elemanı değerini dosya ismi olarak kullanıyoruz.
save metodumuz (bu arada bir değişkene bağlı çalışan fonksiyonlara çoğunlukla metod denir), error tipinde bir değer döner. Çünkü bu bir dosyaya byte slice değerini yazan standart os fonksiyonu WriteFile()'ın dönen değeri. Bu dönen değeri uygulamamızda kullanarak dosyaya yazma işleminde olabilecek hataları algılayabiliriz. Eğer dosyaya kayıt işlemi düzgün giderse Page.save() metodu nil değer dönecektir.
Octal (sekizli sistem) sabit değer 0600 ise WriteFile() fonksiyonuna üçüncü parametre olarak veriliyor. Bu sistemde o andaki kullanıcı için dosyanın okuma/yazma izni ile açılmasını sağlıyor.
Dosyaya kaydettiğimiz gibi sayfaları dosyadan geri okumak için de bir fonksiyon yazmamız gerekiyor.
loadPage() fonksiyonu dosya adını kendisine verilen title parametresinden üretir. Dosyanın içeriğini body isimli değişkene okur. Sonra bu elindeki body ve title değerleri ile oluşturduğu Page tipinde bir değer geri döner.
Fonksiyonlar birden fazla değer dönebilir, buradaki standart os.ReadFile() fonksiyonu da bir []byte (byte slice) ve bir error tipi değer döner. Şimdilik ReadFile() fonksiyonundan hata dönmesini işlemeyeceğimiz için error tipi dönen değeri eşitliğin solunda alt çizgi ile göstererek kullanmayacağımızı belirttik.
Fakat ya ReadFile() bir hataya sebep olursa örneğin verilen isimde bir dosyayı hard diskte bulamazsa? Bunu göz ardı edemeyiz fonksiyonumuzu Page değerden başka bir de error tipi değer dönecek şekilde değiştirelim.
Artık bu fonksiyonu çağıran kod ikinci dönen değerden hatayı algılayabilir. Eğer değer nil değilse dosya okunamamış demektir.
Bu noktada basit bir örnek veri ile fonksiyonlarımızı test etmek için bir main() fonksiyonu yazalım.
Bu kodu derleyip çalıştırdığımızda klasörde TestSayfa.txt adında yeni bir dosya gelmelidir. Bunun içeriği p2 değişkenine okunacak ve onun Body elemanı da ekrana yazdırılacak.
Derleyip deneyelim.
(Windows kullanıyorsanız başında "./" olmadan sadece wiki yazıp enter basmanız yeterli olacaktır.)
net/http Paketinde Ne Var?
Bu noktada kısa bir ara verip net/http paketindeki fonksiyonların bir web server yapmak için nasıl kullanıldığına bir göz atalım.
Çalışan bir server için örnek kod şöyle :
main fonksiyonu bir http.HandlerFunc() fonksiyonu çağırarak başlıyor. Bu root adrese ("/") yapılan isteğe hangi metodla cevap verileceğini belirtiyor.
Daha sonra http.ListenAndServe() fonksiyonu çağrılarak web server işlemine port olarak 8080'i kullanarak giriliyor. Şimdilik ikinci parametreye verilen nil değerini geçelim. Program bu noktada web server'ı çalıştırarak sonlandırılıncaya kadar orada takılı bekler.
ListenAndServe() fonksiyonu beklenmeyen bir hata oluşursa bir hata mesajı döner, bu mesajı konsola yazdırmak için de log.Fatal() fonksiyonunu kullanıyoruz.
handler fonksiyonu veri tipi http.HandlerFunc. Bir http.ResponseWriter tipi ve bir de http.Request tipi parametre alıyor, çünkü bir HandlerFunc tipi fonksiyonu çağıran server rutini fonksiyona bu değerleri gönderir.
Burada w değişkeni adı verilen http.ResponseWriter bizim isteği yapan tarayıcıya (http client) vereceğimiz cevabı oluşturur. Bu değişkene bir şeyler yazdığımızda o değer client'a gönderilir.
Bir http.Request ise tarayıcı tarafından server'a gönderilen isteğin bilgilerini içeren bir struct yapıdır. r.URL.Path ise adres bilgisinin path kısmıdır - mesela "http://www.turkpoem.com/products.html" için path "/products.html" olur. Burada [1: ] ile ilk karakter yani bölü işareti çıkartılıp geri kalan alınıyor.
Eğer bu programı çalıştırır ve örneğin
adresini tarayıcınızda açarsanız.
net/http ile wiki Sayfalarımızı Yayınlamak
Öncelikle , kullanmak için kütüphaneyi import etmeliyiz.
Şimdi de kullanıcıya wiki sayfamızı gönderecek olan viewHandler() fonksiyonumuzu tanımlayalım. Bu fonksiyonda URL adres path değeri "/view/" ile başlayan adresleri cevaplayacağız.
Öncelikle bu fonksiyon r.URL.Path değerinden sayfanın başlığını (title) buluyor. Bu başlıktaki dosyayı loadPage() fonksiyonumuz ile açıp okuyoruz.
En son da http.ResponseWriter tipi w değişkenine bu elimizdeki bilgilerden oluşturduğumuz web sayfasının HTML kodunu dönüyoruz.
Bu olay işleyici fonksiyonu (handler) kullanmak için main fonksiyonumuzu da örnek server kodu gibi değiştirelim.
Daha önce deneme yaparken TestSayfa.txt adında bir wiki sayfası örneği yapmıştık, şimdi programı çalıştıralım,
Tarayıcımızda
sayfasını açarsak,
ama adrese mesela
yazarsanız çakılır. Konsola da bir sürü hata mesajı yazar. Çünkü o isimde bir dosya yok ve loadPage() fonksiyonu hata verir.
Bir de "/view/" ile başlayandan farklı adres girerseniz mesela
Bu adrese karşılık bir handler fonksiyon tanımlanmadığı için server 404 döner.
Wiki Sayfalarını Düzenlemek
Bir wiki uygulaması sayfaları düzenlemek kabiliyeti olmadan eksiktir. Bu amaçla 2 tane handler fonksiyon daha ekleyelim, biri sayfayı düzenlemek için "/edit/" adresleri işleyen, diğeri de sayfayı diske kaydetmek amacıyla kullanılacak "/save/" adresleri işleyen fonksiyon olacak.
Öncelikle fonksiyonlar için main fonksiyonumuza ilaveleri yazalım.
editHandler() fonksiyonumuz eğer sayfa mevcutsa onu yükler, ama eğer mevcut değilse boş bilgiler ile yeni bir yazı oluşturur ve bunu HTML form elemanları ile kullanıcıya sunar.
Bu fonksiyon sağlıklı çalışır ama HTML kodlarını böyle string içine gömmeye çalışmak hem zor hem de çirkin bir görüntü. Bunun başka bir yolu daha var.
html/template Şablon Kullanma Paketi
html/template paketi Go'nun standart kütüphanelerinden biridir. Bu kütüphane yardımıyla HTML kodlarımızı ayrı dosyalarda saklayarak ana kodumuz içinde karmaşıklık olmasının önüne geçeriz. Hem bu sayede Go kodumuzu kurcalamadan görsel sayfa kodlarını düzenleyebiliriz.
Öncelikle html/template paketini import ederek başlayalım , ayrıca fmt paketini kullanmadığımız için onu da listeden çıkaralım.
Öncelikle HTML formumuzu içeren bir şablon (template) dosya oluşturalım. İsmi edit.html olsun.
Şimdi HTML kodları çok daha açık görebiliyoruz. editHandler() fonksiyonumuzu bu şablonu kullanacak şekilde değiştirelim.
template.ParseFiles() fonksiyonu "edit.html" dosyasının içeriğini okur ve bir *template yapısı değer olarak (şablon olarak) döner.
t.Execute() fonksiyonu bu şablonu işler ve sonucu w değişkenine yani http.ResponseWriter'a yazarak server'ın client'a cevabını gönderir. Şablon içinde verilen .Title ve .Body referansları p.Title ve p.Body değerlerini gösterir.
Şablon içinde değişken değerlerine yönlendirmeler çift süslü parantez içinde yapılır. Aslında süslü parantezler içerisindeki Go kodu çalıştırılır ve sonuç oraya yazılır. {{.Title}} yerine p.Title değeri yazılır mesela, ve {{printf "%s" .Body}} ifadesi de fmt.Printf() gibi formatlanmış bir yazı çıktısı oluşturur. html/template paketi oluşturduğu HTML kodlarının güvenli olmasını da sağlar. Örneğin yazı içindeki büyüktür işaretlerini (>) > karakterleri ile değiştirerek kullanıcının yazdıklarının içinde HTML kod enjeksiyonu olmamasını sağlar.
Şablonları incelediğimize göre viewHandler() fonksiyonumuzda da şablon kullanalım. view.html adında bir şablon dosya ekleyelim.
karşılık şekilde viewHandler() fonksiyonumuzu da değiştirelim.
Dikkat ettiyseniz her iki olay işleyicide de şablon kullanmak için neredeyse aynı kodları kullandık. Hadi bu ortak kod için yeni bir fonksiyon yazıp onu kullanalım.
Eğer henüz karşılığı yazılmamış olan saveHandler() olay işleyicisini yoruma atarsak programın şu ana kadarki kısmını deneyebiliriz. Kodumuzun en son ulaştığı şekil şöyle:
Mevcut Olmayan Sayfaların İşlenmesi
Şimdi http://localhost:8080/view/OlmayanSayfa adresini açmaya kalksanız ne olur? Size boş bir sayfa döner. Çünkü kod şu anda loadPage() fonksiyonundan bir hata dönmesi durumuna bakmıyor boş bilgilerle kullanıcıya bir sayfa dönmeye çalışıyor. Eğer istenen sayfa yoksa kullanıcıyı edit sayfasına yönlendirirsek yeni bir sayfa olarak yazmaya başlayabilir.
http.Redirect() fonksiyonu HTTP cevabına bir durum kodu (http.StatusFound - 302) ve bir yönlendirme header bilgisi ekler ve tarayıcıyı bildirilen sayfaya gitmesi için yönlendirir.
Sayfaların Kaydedilmesi
saveHandler() fonksiyonu sayfa düzenlemesi yapılan edit sayfasından gönderilen form bilgileriyle wiki sayfasını kaydedecek. Test etmek için main fonksiyonunda yoruma aldığımız satırı geri alalım ve fonksiyonu yazalım.
Adres bilgisinden alına title ve formda gönderilen yegane değer olan body bilgileriyle Page tipi veri hazırlanıyor. p.save() fonksiyonumuz ile server hard diskine kaydediliyor. Server hard diskine bir şeyler kaydedilmesi demek web uygulamalarınızda bu formlardan gelen bilgileri ne kadar ciddi takip etmeniz gerektiğini açıklıyordur herhalde.
En sonda ise kaydedilen wiki sayfasının gösterildiği sayfaya bir yönlendirme ile gidiliyor. Gördüğünüz üzere saveHandler() fonksiyonumuz bir görsel şablon kullanmıyor, kayıt işlemini yapıyor ve kaydı yapılan sayfaya yönlendiriyor.
Son bir şey , formdan gelen veri bir string veri olduğu için onu Page yapısındaki Body elemanına kaydedebilmek için []byte slice yapmamız gerekiyor tabii ki.
Hataları İşlemek
Programımızda hataları göz ardı ettiğimiz yerler oldu. Programımızın beklenmedik durumlar karşısında davranışını belirlemeliyiz. Bunun için en uygun yöntem hataları algılamak ve uygun bir mesajla kullanıcıya bildirmek. Böylece hem server'ın çalışmaya devam etmesini hem de kullanıcıyı sorun hakkında bilgilendirmeyi başarabiliriz.
Öncelikle şablonlarımızı yayınlayan renderTemplate() fonksiyonundan başlayalım.
Mesela ilk hata şablon dosyasını parse ederken oluşan hatalar için. Bunu test amacıyla örneğin view.html kodunda "%s" yazısında tırnakları koymayı unutalım.
Şimdi server'ı çalıştırıp denediğimizde :
http.Error() fonksiyonu belirtilen HTTP durum kodunu da içeren (burada InternalServerError) bir hata mesajını kullanıcıya döner.
Sırada saveHandler() fonksiyonumuz var.
Bu da dosya kaydetme hatalarını verir. Mesela olmaz ama dnm.txt adında bir klasör var ve siz dnm sayfası kaydetmeye kalkarsanız hata verir.
Şablonu Tampon Hafızaya Almak
Belki deneme yaparken dikkatinizi çekmiştir, renderTemplate() fonksiyonu içinde şablon her seferinde parse edildiği için server çalışırken bile şablon kodunda bir değişiklik yapsanız görsel yenilenince o değişikliği görürsünüz. Böyle net'ten her istek yapana karşı şablon parse etmek, çok kullanıcılı sistemleri korkunç yavaşlatır. Bunu önlemek için şablonların parse edilmiş hallerini hafızada saklayıp , istenildiğinde kullanmak çok daha mantıklı olacak.
Öncelikle templates adında global bir değişken tanımlayıp şablonların parse edilmiş hallerini onda toplayalım.
template.Must() fonksiyonu , eğer parametresinde yapılan işlemden hata gelirse panic üreten yoksa *Template yapısında değer dönen yardımcı bir fonksiyon. Yani şablonlarımızda da hata varsa server baştan panic verip çakılsın daha iyi aslında.
ParseFiles() fonksiyonu adından da anlaşıldığı üzere kodumuzda kullandığımız gibi tek dosya değil birçok dosyayı da parse edebilir. Dönen değerde her işlenen şablonun dosya adı ile bir dizi şablon vardır. Eğer uygulamamızda başka şablonlar da kullanmış olsaydık, onları da templates struct değişkeni elemanları olarak ekleyebilirdik.
Şimdi renderTemplate() fonksiyonumuzu bu hafızaya alınmış templates değişkeni elemanından ilgili şablonu alıp kullanacak şekillde değiştirelim. Bu amaçla templates.ExecuteTemplate() metodu kullanılıyor.
Gördüğümüz gibi templates değişkeni içindeki ilgili şablonumuzun ismi, kullanılan dosya ismi ile aynı.
Artık şablonlarımız sadece programımız ilk çalıştığı anda işlenecek , daha sonra şablon dosyada değişiklik yaparsanız server'ı (uygulamamızı) tekrar başlatmamız gerekecektir. Ama server artık binlerce kullanıcıya çok daha hızlı cevap verebilir.
Doğrulama (Validation - Validasyon - Validatzione)
Görmüş olduğunuz gibi bu program çok büyük bir güvenlik açığına sahip. Kullanıcı gireceği adres bilgisi yardımıyla server'da herhangi bir klasöre erişip okuma yazma yapabilir. Bunu kısıtlamak için girilen title değerinin istediğimiz özelliklerde olması için bir regex karşılaştırması yapabiliriz. Öncelikle regexp kütüphanesini import listemize ekleyelim ve bizim için geçerli dosya yolunu belirten bir regex eşleşmesini global değişken olarak tanımlayalım.
regexp.MustCompile() fonksiyonu parametresinde verdiğimiz regex ifadeyi derler ve regexp bir değer döner. MustCompile çalışma olarak Compile'dan farklıdır. Compile ifadeyi derlerken hata olursa ikinci dönen değer olarak error değeri dönerken, MustCompile derlerken hata olursa panic verip çalışmayı durdurur.
Şimdi validPath eşleşmesini kullanarak title değerinin doğrulamasını yapan ve title değerini dönen bir fonksiyon yazalım.
Eğer title değeri geçerliyse tek başına döner, dönen error değer nil olur. Burada errors paketinin fonksiyonunu kullanıyoruz , lütfen onu da import listemize ekleyelim.
Eğer title değeri kriterimize uymuyorsa kullanıcıya "404 Not Found" mesajı gönderilir ve olay işleyici fonksiyona (handler) bir hata dönülür.
Şimdi olay işleyici fonksiyonlarımızı title değerini doğrulayarak kullanacak hale getirelim.
Şimdi uygulamayı çalıştırıp tarayıcıdan geçersiz bir isim , mesela
verip başka klasöre geçmek istesek
Fonksiyon Literaller ve Closure'lar
Olay işleyici fonksiyonlarımızda hala tekrarlanan kodlar var. title değerinin işlenmesi ve doğrulaması birbiriyle aynı. Bu konuda Go fonksiyon literal yapısını kullanarak bir kısaltmaya daha gidebiliriz. Maksat öğrenmek olsun.
Öncelikle 3 fonksiyonumuzu da title stringini parametrede alacak şekilde değiştirelim.
Şimdi üsttekiler gibi tanımlanmış bir fonksiyonu alıp http.HandlerFunc tipi bir fonksiyon dönen yeni bir fonksiyon tanımlayacağız. Olay işleyicilerin yapısını bozduk, bu yüzden http.HandleFunc() çağrılarımıza uygun parametre olabilecek bir fonksiyon lazım bize.
Burada geri dönen fn fonksiyonuna closure denir. Çünkü kendisi dışında tanımlanan değerleri kullanır. Şimdi sondan başa gidelim, bu makeHandler() bize bir olay işleyici fonksiyon (http.HandlerFunc tipi) dönecek ve biz onu main fonksiyonumuzda şöyle kullanacağız.
makeHandler() fonksiyonu bir httpHandlerFunc tipi olduğuna göre iki parametresi olacak (http.ResponseWriter ve *http.Request tipinde iki değer). Geri döndüğümüz fn fonksiyonu da bu iki değeri aynen kullanır (kopyasını değil direk kendisini). Bu durumda fn fonksiyonu içinde mesela w değerine yazdığımız şey direk client'a cevap olarak dönecektir.
Şimdi makeHandler() fonksiyonumuz içinde title ile ilgili işlemleri yapıp ilgili save, edit ya da view olay işleyicimizi geri dönebiliriz (gerçi onlar artık olay işleyici değil, düz fonksiyonlar, onları olay işleyici haline makeHandler() dönüştürüyor).
getTitle() içindeki kodları biraz modifiye ederek makeHandler() içinde kullanalım.
makeHandler() tarafından dönülen closure fonksiyon bir http.HandlerFunc (yani bir http.ResponseWriter ve bir http.Request tipi parametre alır). Closure fonksiyon yapılan http isteğinden title değerini bulur ve onun geçerli regexp ile doğrulamasını yapar. Eğer title değeri geçersizse http.NotFound() fonksiyonu ile kullanıcıya bir hata mesajı döner. Eğer title hatalı değilse , eldeki bilgilerle ilgili işleyici fonksiyon çağrılır.
Şimdi fonksiyonlarımızdan getTitle kısımlarını uzaklaştırarak daha basit görünmesini sağlayabiliriz.
Kodu deneyip çalışmasını test edebilirsiniz.
Bu yazı da burada bitti. Yeni bir şeyler öğrendik umarım. Tekrar görüşene kadar Kalın Sağlıcakla..
Hiç yorum yok:
Yorum Gönder