https://www.buraksenyurt.com/Burak Selim Şenyurt - GoLang2020-12-07T20:23:51+00:00Matematik Mühendisi Bir Bilgisayar Programcısının NotlarıBurak Selim SenyurtBlogEngine.Net Syndication Generatorhttps://www.buraksenyurt.com/opml.axdBurak Selim SenyurtMatematik Mühendisi Bir Bilgisayar Programcısının Notlarıtr-TRBurak Selim Şenyurt0.0000000.000000https://www.buraksenyurt.com/post/go-ortaminda-minik-bir-crud-servisini-gin-gonic-ile-gelistirmekGO Ortamında Minik Bir CRUD Servisini Gin-Gonic ile Geliştirmek2020-09-03T13:00:00+00:00bsenyurt<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/gingonic.png" alt="" align="right" />Gin-Gonic hafif siklet sayılan ama yüksek performansı ile öne çıkan<em>(Muadili olan martini'den çok daha hızlı olduğu ifade ediliyor. Bu arada farklı Http Web Framework'ler için <a href="https://deepsource.io/blog/go-web-frameworks/" target="_blank">şu yazıyı</a> inceleyebilirsiniz)</em> bir HTTP-Web framework. Elbette açık kaynak bir çatı. Middleware tarafında<em>(Yani Request ve Response'lar arasında)</em> Recover ve Log desteği sunuyor. Tabii kendi middleware bileşenimizi yazıp ekleyebiliriz de. Recovery modülü en başından beri ekli olduğundan paniklemeyen bir framework diyebiliriz :) Yani Go çalışma zamanında HTTP talepleri ile ilgili olarak bir panic oluştuğunda uygun bir 500 cevabı verebiliyor.</p>
<p>Söylentilere göre bu özelliği sayesinde söz konusu servis her an ayakta ve çalışır durumda kalıyor<em>(muş)</em>. Bahseliden bu yeteneklere ilaveten yönlendirmeleri<em>(routes)</em> gruplandırabiliyoruz ki bu da örneğin versiyonlamayı kolaylaştırıyor. Bu kısa notlar şimdilik yeterli. Sahada deneyimlemek lazım. İşte bu <a href="https://github.com/buraksenyurt/skynet" target="_blank">SkyNet</a> derlemesindeki amacımız MongoDb üzerinde basit CRUD<em>(Create Read Update Delete)</em> işlemlerini yaparken gin-gonic üstüne kurulmuş golang tabanlı bir servis geliştirmek. Tahmin edeceğiniz üzere ben örneği Heimdall<em>(Ubuntu-20.04)</em> üstünde geliştiriyorum. MongoDb tarafı içinde bir Docker imajını kullanmayı tercih edeceğim.<em> </em>Dilerseniz vakit kaybetmeden idmanımıza başlayalım. İşte ilk terminal adımlarımız.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># Ana klasörümüz ve gerekli go dosyaları oluşturulur
mkdir book-worm-api
cd book-worm-api
touch main.go
# gin-gonic ve diğer modüllerin yönetimi için
# mod uzantılı bir dosya oluşacaktır. Burada yüklediğimiz paket bilgilerini görebiliriz.
# Genel isimlendirme standardı olarak github.com/buraksenyurt/book-worm-api kullanımı da tercih edilebilir
go mod init book-worm-api
# gin-gonic ve mongodb için gerekli go paketlerinin yüklenmesi
go get -u github.com/gin-gonic/gin go.mongodb.org/mongo-driver
# MongoDB tarafıyla eşlecek entity için
touch quote.go
# CRUD Operasyonları için
touch quotedata.go
# Servis metotlarındaki annotation bildirimlerinin Swagger 2.0 destekli olarak dokümante edilmesi için gerekli modülün eklenmesi
go get -u github.com/swaggo/swag/cmd/swag
# Bu arada kod tarafındaki annotation bölümleri tamamlandıktan sonra Swagger dokümanının üretilmesi için aşağıdaki komutu çalıştırmalıyız
swag init _ "book-worm-api/docs"
# DOCKER TARAFI
# mongodb docker container'ının çalıştırılması ve veritabanının oluşturulması
# bookworms isimli bir veritabanı oluşturuyoruz ve root user ile password bilgisi de veriyoruz (Bunu production'da yapmayın tabii)
sudo docker run --name mongodb -e MONGO_INITDB_ROOT_USERNAME=scoth -e MONGO_INITDB_ROOT_PASSWORD=tiger -e MONGO_INITDB_DATABASE=bookworms -p 27017:27017 -d mongo:latest</pre>
<p>Gelelim kod tarafında yaptıklarımıza. Entity tipimiz olan quote yapısını<em>(struct)</em> aşağıdaki şekilde geliştirebiliriz.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import "go.mongodb.org/mongo-driver/bson/primitive"
/*
quotation yapısı mongodb'deki dokümanın GO tarafındaki izdüşümü.
ID, mongodb tarafı için bson'un primitive tiplerinden birisi ile ifade edilebilir
*/
type quote struct {
ID primitive.ObjectID
Description string
Writer string
Book string
}</pre>
<p>CRUD operasyonlarını ihtiva eden ve MongoDB tarafı ile haberleşen fonksiyonları içeren quoteData.go dosyasının içeriğini de aşağıdaki şekilde hazırlayabiliriz.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
/*
Burada MongoDB ile ilgili CRUD Operasyonları yer alıyor.
*/
import (
"context"
"log"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// getConn fonksiyonu ile mongodb bağlantı nesnesini ve context'i döndürüyoruz
func getConn() (*mongo.Client, context.Context) {
/*
NewClient nesnesini docker ile ayağa kalkan mongodb adresine göre oluşturuyoruz
bir error olması durumu da aşağıya doğru sürekli kontrol edilmekte
*/
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://scoth:tiger@localhost:27017")) // ilgili bağlantı bilgisini kullanarak yeni bir client nesnesi oluşturuldu
ctx := context.Background()
err = client.Connect(ctx)
if err != nil {
log.Printf("Bağlanırken hata alındı: %v", err)
}
// Bağlantıyı kullanarak mongodb'yi ping'liyoruz
err = client.Ping(ctx, nil)
if err != nil {
log.Printf("Ping yapılamadı: %v", err)
}
return client, ctx // geriye Context nesnesini de dönüyoruz. İzleyen metotlardaki defer kullanımlarına dikkat!
}
// AddQuote tahmin edileceği üzere bookworm veritabanındaki quotes koleksiyonuna yeni bir quote eklemek için kullanılıyor
func AddQuote(q *quote) (primitive.ObjectID, error) {
log.Println("Add Quote")
log.Println(q)
client, ctx := getConn()
defer client.Disconnect(ctx) // AddQuote işleyişinin sonunda mongodb bağlantısının kapatılmasını garanti ediyoruz
q.ID = primitive.NewObjectID() // eklenecej doküman için yeni bir Object ID üretiliyor ve parametre olarak gelen quote değişkenine yapıştırılıyor.
// InsertOne ile q ile gelen quote değişkenini yolluyoruz. Eğer bir sorun olursa err parametresi hata bilgisini taşıyacaktır
result, err := client.Database("bookworms").Collection("quotes").InsertOne(ctx, q)
if err != nil { // Eğer hata olmuşsa bunu metottan geriye nil object ID ile birlikte dönüyoruz. Hatayı gin-gonic metotları değerlendirip uygun HTTP mesajını döndürecektir.
log.Printf("Alıntı eklenmeye çalışırken hata oluştu %v", err)
return primitive.NilObjectID, err
}
id := result.InsertedID.(primitive.ObjectID)
return id, nil // Eğer sorun yoksa eklenen Object ID bilgisini dönüyor. Bu noktada hata olmadığı için ikinci output değişkeni nil olarak atanıyor
}
// GetQuotes metodu ile bookworm veritabanındaki quotes koleksiyonunda yer alan tüm dokümanları çekiyoruz
// Basit bir veri çekme metodu da olsa her ihtimale karşı hata kontrolümüz de var
func GetQuotes() ([]*quote, error) {
var quotes []*quote // Döndüreceğimiz array
client, ctx := getConn() // MongoDb bağlantı bilgilerini aldık
defer client.Disconnect(ctx) // Panik olsa da olmasa da metot tamalanırken Disconnect olalım
db := client.Database("bookworms") //veritabanı nesnesi
collection := db.Collection("quotes") // koleksiyon nesnesi
cursor, err := collection.Find(ctx, bson.D{}) // quotes koleksiyonundaki tüm dokümanları çekmek için kullanılan fonksiyon
if err != nil { // Find metodunun da error dönüşü var o yüzden kontrol etmekte fayda var
return nil, err
}
defer cursor.Close(ctx) // Eğer hata almadan geldiysek sonraki hata durumuna karşın Find ile açılan cursor'u kapattırır
err = cursor.All(ctx, "es) // Koleksiyonu quotes'a alıyoruz
if err != nil { // All metodunun da error dönüşü var. Kontrol etmek iyi fikir
log.Println(err)
return nil, err
}
return quotes, nil // Her şey yolunda gittiyse ;)
}</pre>
<p>Gelelim tüm bunları kullanacağımız ana yüklenici main modülünün içeriğine. Swagger için annotation'lar kullandığımız dikkatinizden kaçmasın.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
// gin-gonic modülünü ekledik
// Ayrıca Swagger desteği için gin-swagger modülü de eklendi
import (
"log"
"net/http"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
_ "book-worm-api/docs" //swag init ile eklenen dokümanın bildirimi
// Bunu eklememin sebebi çalışma zamanında swagger UI'a gidince aldığım Failed to load spec. hatası
"github.com/gin-gonic/gin"
)
// @title BookWorm Swagger API
// @version 1.0
// @description Servis kullanım rehberidir
// @termsOfService http://swagger.io/terms/
// @contact.name Burak Selim Şenyurt
// @contact.email selim@buraksenyurt.com
// @contact.url https://www.buraksenyurt.com
// @BasePath /api/v1
// @host localhost:5003
func main() {
router := gin.Default() // gin nesnesini örnekledik
api := router.Group("/api") //ve api'nin
v1 := api.Group("/v1") // v1 sürümü için
quotes := v1.Group("/quote") //quotes isimli bir route tanımladık
// Bu route için kök adrese HTTP Get talebi gelirse tüm alıntıları listeyecek operasyonu çalıştırıyoruz
quotes.GET("/", GetAll)
// Kök adrese bir Post talebi gelirse bu sefer yeni bir alıntının eklenmesini sağlıyoruz
quotes.POST("/", Create)
/*
Swagger API dokümantasyon desteği için ekledik.
metot başlarında yer alan yorum satırları da Swagger UI tarafında değerlenecek
*/
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
_ = router.Run(":5003") // sunucuyu 5003 numaralı porttan hizmete açtık
}
/*
@Summary operasyon için açıklama kısmıdır.
@Produce ve @Accept ile HTTP operasyonun hangi tür içerikle çalıştığını söylüyoruz ki örnekte json.
@Param kısmında Body içinden quote tipinden bir değişken beklediğimizi ve zorunlu olduğunu ifade ederiz
@Sucess ile operasyon başarılı ise HTTP 200 dönüldüğünü ifade ederiz
@Failure kısımlarında metottan hangi tür HTTP hatalarının dönebileceğini ifade ediyoruz
@Router kısmında operasyon adresini HTTP POST metodu ile tetiklendiğini ifade ediyoruz
*/
// Create godoc
// @Summary Yeni bir kitap alıntısı ekler
// @Produce json
// @Accept json
// @Param quote body quote true "Alıntı Bilgileri"
// @Success 200
// @Failure 400
// @Failure 500
// @Router /quote/ [post]
func Create(c *gin.Context) {
var q quote
// Gelen JSON içeriğinin quote yapısına eşlenip eşlenemediği kontrol ediliyor
if err := c.ShouldBindJSON(&q); err != nil {
log.Print(err)
c.JSON(http.StatusBadRequest, gin.H{"msg": err}) // Eğer JSON içeriğinde sıkıntı varsa hata mesajı ile birlikte geriye HTTP 400 Bad Request dönüyoruz
return
}
id, err := AddQuote(&q) // Add metodu ile eklemeyi gerçekleştiriyoruz
if err != nil { // eğer hata varsa bunu da değerlendirip geriye uygun bir HTTP durum kodu ile döndürüyoruz
c.JSON(http.StatusInternalServerError, gin.H{"msg": err})
return
}
q.ID = id
c.JSON(http.StatusOK, gin.H{"added": q}) // Her şey yolundaysa HTTP 200 Ok
}
/*
{array} ile bir quote listesinin döneceğini belirtiyoruz
*/
// GetAll godoc
// @Summary Tüm kitap alıntılarını döndürür
// @Produce json
// @Success 200 {array} quote
// @Failure 500
// @Router /quote/ [get]
func GetAll(c *gin.Context) {
// mongodb ile iletişim kuran quotedata içerisindeki ilgili metodu çağırdık
var quoteList, err = GetQuotes()
// Her ihtimale karşı listeyi çeken metot bir hata döndürmüş mü bakalım
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"msg": err})
return
}
c.JSON(http.StatusOK, gin.H{"quotes": quoteList}) // HTTP 200 OK ile birlikte çekilen listeyi geri yolluyoruz.
}</pre>
<p>Bu bir servis uygulaması olduğu için çalıştırdıktan sonra bir şekilde tüketmemiz de gerekiyor ki sonuçları görebilelim. curl, postman gibi araçlar bu amaçla kullanılabilir veya bir client uygulama yazılabilir<em>(yazabilirsiniz) </em>Şimdi aşağıdaki terminal komutları ile önce uygulamamızı build edelim ve ardından servisimizi çalıştıralım. Tabii bu işler sırasında MongoDb docker container'ının çalıştığından emin olalım.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># önce bir build etmek lazım
go build
# sonrasında çalıştırabiliriz
./book-worm-api</pre>
<p>İlk testler için aşağıdaki curl komutlarını kullanabiliriz.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false"># Örnek birkaç alıntı girelim
curl --location --request POST 'http://localhost:5003/api/v1/quote/' \
--header 'Content-Type: application/json' \
--data-raw '{
"Description": "Bizler bugüne kadar inşa edilmiş en iyi zaman makineleriyiz.",
"Writer": "Dean Buonomono",
"Book": "Beyniniz: Bir Zaman Makinesi"
}'
curl --location --request POST 'http://localhost:5003/api/v1/quote/' \
--header 'Content-Type: application/json' \
--data-raw '{
"Description": "Böylece, günler geçiyor.",
"Writer": "Albert Camus",
"Book": "Tersi ve Yüzü"
}'
curl --location --request POST 'http://localhost:5003/api/v1/quote/' \
--header 'Content-Type: application/json' \
--data-raw '{
"Description": "Herkes hayatının bir devresinde şu veya bu şekilde talihinin şuuruna erer.",
"Writer": "Ahmet Hamdi Tanpınar",
"Book": "Saatleri Ayarlama Enstitüsü"
}'
# Şimdi de listeleme yapalım
curl http://localhost:5003/api/v1/quote/</pre>
<p>İşte çalışma zamanından birkaç görüntü.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/Screenshot_1.png" alt="" /></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/Screenshot_2.png" alt="" /></p>
<p>Uygulamamıza swagger entegrasyonu yaptığımızdan detaylı bir dokümantasyona da sahibiz. Bu servis ne yapıyor, hangi operasyonları nasıl test ederim, test için bana vereceği curl çıktıları neler vb şeklindeki soruların karşılığı olan dokümantasyonu hatırlayacağınız üzere aşağıdaki terminal komutu ile üretmiştik. Tekrardan hatırlatmakta yarar var.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># İlgili annotation bölümleri tamamlandıktan sonra dokümanın üretilmesi için aşağıdaki komutu çalıştırmalıyız.
swag init _ "book-worm-api/docs"</pre>
<p>Bu işlem docs klasörünün oluşmasını sağlar. Klasördeki swagger.json ve yaml içeriklerini bir inceleyin derim ;) Swagger implementasyonu sonrası çalışma zamanına ait birkaç ekran görüntüsünü de aşağıda bulabilirsiniz. İşte localhost:5003/swagger/index.html den erişebileceğimi ana sayfadan bir görüntü. Gayet şık değil mi? :) </p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/Screenshot_3.png" alt="" /></p>
<p>Yeni bir kitap alıntısı eklerken ki görüntü.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/Screenshot_4.png" alt="" /></p>
<p>Post işleminin sonucuna ait bir görüntü.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/Screenshot_5.png" alt="" /></p>
<p>ve son olarak tüm kitap alıntılarını çektiğimiz HTTP Get talebine ait görüntü.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/25/Screenshot_6.png" alt="" /></p>
<p>Örnek servisimiz MongoDb'yi gin-gonic çatısı üstünden kullanan bir Go uyglaması. Swagger desteği de bulunmakta. Eğer Update ve Delete operasyonlarını da işin içerisine katarsak, çalıştığımız kurumlar için hafif/orta siklet sayılabilecek REST Api'leri kolaylıkla geliştirebiliriz. Böylece geldik bir SkyNet derlemesinin daha sonuna. Örnek uygulama kodlarına <a href="https://github.com/buraksenyurt/skynet/tree/master/No%2025%20-%20Hello%20Gin-Gonic" target="_blank">github reposu üzerinden</a> erişebilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2020-09-03T13:00:00+00:00go programming languagegolanggogingonicgin-gonicrestrest apirest based servicesswaggerhttphttp web frameworkbsenyurtGin-Gonic hafif siklet sayılan ama yüksek performansı ile öne çıkan(muadili olan martini'den çok daha hızlı olduğu ifade ediliyor) bir HTTP-Web framework. Elbette açık kaynak bir çatı. Middleware tarafında(Yani Request ve Response'lar arasında) Recover ve Log desteği sunuyor. Tabii kendi middleware bileşenimizi yazıp ekleyebiliriz de. Recovery modülü en başından beri ekli olduğundan paniklemeyen bir framework diyebiliriz :) Yani Go çalışma zamanında HTTP talepleri ile ilgili olarak bir panic oluştuğunda uygun bir 500 cevabı verebiliyor.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=829e1816-6e1e-4aec-8c38-05692d04c5b90https://www.buraksenyurt.com/trackback.axd?id=829e1816-6e1e-4aec-8c38-05692d04c5b9https://www.buraksenyurt.com/post/go-ortaminda-minik-bir-crud-servisini-gin-gonic-ile-gelistirmek#commenthttps://www.buraksenyurt.com/syndication.axd?post=829e1816-6e1e-4aec-8c38-05692d04c5b9https://www.buraksenyurt.com/post/mongodb-ile-bir-go-uygulamasini-konusturmakMongoDB ile Bir GO Uygulamasını Konuşturmak2019-12-18T08:00:00+00:00bsenyurt<div><img style="float: right;" src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_9.png" alt="" /></div>
<div>Teknoloji baş döndüren bir hızla ilerlerken beynimizin tembelleştiğini de kabul etmemiz gerekiyor. Artık pek çok işimiz otonomlaştırıldığından zihnimiz eski egzersizleri yapmıyor. Yıllar önce İngiltere'de yapılan bir araştırmada çocukların hesap makinesi kullanması sebebiyle temel dört işlem matematiğinde sorunlar yaşadığı tespit edilmişti. Yine Kanada'da yapılan bir araştırma insanların dikkat dağılma sürelerinin 8 saniyelere kadar indiğini gösterdi. Hafızamızı dinç tutma noktasında Japon balıkları ile yarışır bir konumda olduğumuz da aşikar. Kaçımız aklından ezbere 4 telefon numarasını sayabilir<em>(Üç haneliler yasak)</em> Otonomlaşan dünya sebebiyle tembelleşen ve dış uyarıcılar yüzünden sürekli dikkati dağılan zihnimiz...Gerçekten de dikkatimizi dağıtan, odaklanmamızı bozan o kadar çok şey var ki. Dolayısıyla kendimizi yetiştirmek istediğimiz konulara çalışırken ne kadar verimli olabiliyoruz bir bakmak gerekiyor. Tekrar satın alınamayacak olan zamanın ne kadar kıymetli olduğunu düşünürsek verimli çalışmanın ilerleyen yaşlarda çok çok önemli bir mesele haline geldiğini vurgulamak isterim.</div>
<div> </div>
<div>İşte bu sebepten birkaç haftadır <a href="https://github.com/buraksenyurt/saturday-night-works">Saturday-Night-Works</a> çalışmalarım sırasında pomodoro tekniğini neden kullanmadığımı karar kara düşünmekteyim. Oysa ki çok verimli bir çalışma pratiği. Atladığım bu önemli detayı SkyNet çalışmalarımın ilk gününden itibaren uygulamaya karar verdim. Genellikle gece 22:00 sularında başlayarak 4X25 dakikalık seanslar halinde ilerliyorum. Her seans arasında 5er dakikalık standart molalar var. Tabii bu tekniği uygularken en önemli kural çalışmayı bölecek unsurları mutlak suretle dışarıda bırakmak. Cep/ev telefonu, televizyon, radyo, e-mail programı ve benzeri odak dağıtıcı ne kadar şey varsa kapatmak gerekiyor. Bunun faydasını epeyce gördüğümü ve 25 dakikalık zaman dilimlerindeki çalışmalardan iyi seviyede verim aldığımı ifade edebilirim. Aranızda uygulamayanlar varsa bir göz atsınlar derim ;) </div>
<div> </div>
<blockquote>
<div>Pomodoro tekniğini uygularken size akıllı bir kronometre gerekecek. Tarayıcıda çalışan <a href="https://tomato-timer.com/" target="_blank">Tomato-Timer</a> tam size göre. Hatta Visual Studio Code için eklentisi bile var ;)</div>
</blockquote>
<div>Gelelim SkyNet'te geçirdiğim ikinci güne. Elimizdeki malzemeleri sayalım. MongoDB için bir docker imajı, gRPC ve GoLang. Bu üçünü kullanarak CRUD<em>(Create Read Update Delete)</em> operasyonlarını icra eden basit bir uygulama geliştirmek niyetindeyim. Bir önceki öğretide Redis docker container'dan yararlanmıştım. Kaynakları kıymetli olan Ahch-To sistemini kirletmemek adına MongoDB için de benzer şekilde hareket edeceğim. Açıkçası GoLang bilgim epey paslanmış durumda ve sistemde yüklü olup olmadığını dahi bilmiyorum.</div>
<div>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">go version</pre>
terminal komutu da bana yüklü olmadığını söylüyor. Dolayısıyla ilk adım onu MacOS üzerine kurmak.</div>
<h2>İlk Hazırlıklar<em>(Go Kurulumu ve MongoDB)</em></h2>
<div>GoLang'i Ahch-To adasına yüklemek için <a href="https://golang.org/dl/" target="_blank">şu adrese</a> gidip Apple macOS sürümünü indirmem gerekti. Ben öğretiyi hazırlarken go1.13.4.darwin-amd64.pkg dosyasını kullandım. Kurulum işlemini tamamladıktan sonra komut satırından go versiyonunu sorgulattım ve aşağıdaki çıktıyı elde ettim.</div>
<div> </div>
<div><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_1.png" alt="" /></div>
<div> </div>
<div>Pek tabii içim rahat değildi. Versiyon bilgisi gelmişti ama bir "hello world" uygulamasını çalışır halde görmeliydim ki kurulumun sorunsuz olduğundan emin olayım. Hemen resmi dokümanı takip ederek $HOME\go\src\ altında helloworld isimli bir klasör açıp aşağıdaki kod parçasını içeren helloworld.go dosyasını oluşturdum<em>(Visual Studio Code kullandığım için editörün önerdiği go ile ilgili extension'ları yüklemeyi de ihmal etmedim)</em></div>
<div>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import "fmt"
func main() {
fmt.Printf("Artık go çalışmaya hazırım :) \n")
}</pre>
Terminalden aşağıdaki komutları işlettikten sonra çıktıyı görebildim. </div>
<div>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">go build
./helloworld</pre>
<img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_2.png" alt="" /></div>
<div>Go ile kod yazabildiğime göre MongoDB docker imajını indirip bir deneme turuna çıkabilirim. İşte terminal komutları.</div>
<div>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">docker pull mongo
docker run -d -p 27017-27019:27017-27019 --name gondor mongo
docker container ps -a
docker exec -it gondor bash
mongo
show dbs
use AdventureWorks
db.category.save({title:"Book"})
db.category.save({title:"Movie"})
db.category.find().pretty()
exit
exit</pre>
<img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_3.png" alt="" /></div>
<div> </div>
<div>İlk komutla mongo imajı çekiliyor. İzleyen komut docker container'ını varsayılan portları ile sistemin kullanımına açmak için. Container listesinde göründüğüne göre sorun yok. MongoDB veritabanını container üzerinden test etmek amacıyla içine girmek lazım. 4ncü komutu bu işe yarıyor. Ardından mongo shell'e geçip bir kaç işlem gerçekleştirilebilir. Önce var olan veritabanlarını listeliyor sonra AdventureWorks isimli yeni bir tane oluşturuyoruz. Devam eden kısımda category isimli bir koleksiyona iki doküman ekleniyor ve tümünü güzel bir formatta listeliyoruz. Arka arkaya gelen iki exit komutunu fark etmişsinizdir. İlki mongo shell'den, ikincisi de container içinden çıkmak için.<br /><br />Ah çok önemli bir detayı unuttum! Örnekte gRPC protokolünü kullanacağız. Bu da bir proto dosyamız olacağı ve Golang için gerekli stub içeriğine derleyeceğimiz anlamına geliyor. Dolayısıyla sistemde protobuf ve go için gerekli derleyici eklentisine ihtiyacım var. brew ile bunları sisteme yüklemek oldukça kolay.</div>
<div>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">brew install protobuf
protoc --version
brew install protoc-gen-go</pre>
Kod tarafına geçmeye hazırız ama öncesinde ufak bir bilgi.</div>
<h2>gRPC Hakkında Azıcık Bilgi</h2>
<div>gRPC, HTTP2 bazlı modern bir iletişim protokolü ve JSON yerine ProtoBuffers olarak isimlendirilen kuvvetle türlendirilmiş bir ikili veri formatını kullanmakta<em>(strongly-typed binary data format)</em> JSON özellikle REST tabanlı servislerde popüler bir format olmasına rağmen serileştirme sırasında CPU'yu yoran bir performans sergiliyor. HTTP/2 özelliklerini iyi kullanan gRPC ise 5 ile 25 kata kadar daha hızlı. Bu noktada hatırlamak için bile olsa gRPC ile REST'i kıyaslamakta yarar var. İşte karşılaştırma tablosu.</div>
<div> </div>
<div>
<table style="border: 1px solid black;">
<thead>
<tr>
<td><strong> REST Tarafı</strong></td>
<td style="text-align: right;"><strong>gRPC Tarafı </strong></td>
</tr>
</thead>
<tbody>
<tr style="border: 1px solid black;">
<td style="border: 1px solid black;"> HTTP 1.1 nedeniyle gecikme yüksek </td>
<td style="border: 1px solid black;"> HTTP/2 sebebiyle daha düşük gecikme </td>
</tr>
<tr>
<td style="border: 1px solid black;"> Sadece Request/Response</td>
<td style="border: 1px solid black;"> Stream desteği<em>(Örneğimizde bir kullanımı var)</em></td>
</tr>
<tr>
<td style="border: 1px solid black;"> CRUD odaklı servisler için </td>
<td style="border: 1px solid black;"> API odaklı<em>(Burada CRUD odaklı yapacağız çaktırmayın)</em></td>
</tr>
<tr>
<td style="border: 1px solid black;"> HTTP Get,Post,Put,Delete gibi fiil tabanlı</td>
<td style="border: 1px solid black;"> RPC tabanlı, sunucu üzerinden fonksiyon çağırabilme özelliği</td>
</tr>
<tr>
<td style="border: 1px solid black;"> Sadece Client->Server yönlü talepler</td>
<td style="border: 1px solid black;"> Çift yönlü ve asenkron iletişim</td>
</tr>
<tr>
<td style="border: 1px solid black;"> JSON kullanıyor<em>(serileşme yavaş, boyut büyük)</em></td>
<td style="border: 1px solid black;"> Protobuffer kullanıyor<em>(veri daha küçük boyutta ve serileşme hızlı)</em></td>
</tr>
</tbody>
</table>
<h2>Örnek Uygulama</h2>
</div>
<p>Gelelim kod tarafına... Uygulamanın temel klasör yapısını aşağıdaki gibi oluşturabiliriz. Ben bu işlemleri $HOME\go\src\ altında gerçekleştirdim.</p>
<div>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">mkdir gRPC-sample
cd gRPC-sample
mkdir playerserver
mkdir clientapp
mkdir proto</pre>
playerserver ve clientapp tahmin edileceği üzere sunucu ve istemci uygulama görevini üstleniyorlar. proto klasöründe yer alan player.proto, gRPC mesaj sözleşmesine ait tanımlamaları içermekte. Servis metodları, parametre tipleri ve içerikleri bu dosyada aşağıdaki gibi bildiriliyor. </div>
<div>
<pre class="brush:xml;auto-links:false;toolbar:false" contenteditable="false">syntax="proto3"; //protobuffers v3 versiyonu kullaniliyor
package player; // proto paketinin adi
option go_package="playerpb"; // generate edilecek go paketinin adi
// Player mesaj tipinin tanimi
message Player{
string id=1;
string player_id=2;
string fullname=3;
string position=4;
string bio=5;
}
// Operasyonlarin kullandigi request ve response mesajlarina ait tanimlamalar
message AddPlayerReq{
Player plyr=1;
}
message AddPlayerRes{
Player plyr=1;
}
message EditPlayerReq{
Player plyr=1;
}
message EditPlayerRes{
Player plyr=1;
}
message RemovePlayerReq{
string player_id=1;
}
message RemovePlayerRes{
bool removed=1;
}
message GetPlayerReq{
string player_id=1;
}
message GetPlayerRes{
Player plyr=1;
}
message GetPlayerListReq{}
message GetPlayerListRes{
Player plyr=1;
}
// servis ve operasyon tanimlari
service PlayerService{
rpc GetPlayer(GetPlayerReq) returns (GetPlayerRes);
rpc GetPlayerList(GetPlayerListReq) returns (stream GetPlayerListRes); //server bazlı streaming kullanacağımız için dönüş parametresi stream tipinden
rpc AddPlayer(AddPlayerReq) returns (AddPlayerRes);
rpc EditPlayer(EditPlayerReq) returns (EditPlayerRes);
rpc RemovePlayer(RemovePlayerReq) returns (RemovePlayerRes);
}</pre>
</div>
<div>Bu içeriği Go tarafında kullanabilmek için derlememiz lazım. Derlemeyi aşağıdaki terminal komutu ile gerçekleştirebiliriz<em>(proto dosyasını VS Code tarafında daha kolay düzenlemek için vscode-proto3 isimli extension'ı kullandım)</em></div>
<div>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">protoc player.proto --go_out=plugins=grpc:.</pre>
Proto dosyasının tamamlanmasını takiben playerserver klasöründeki main.go dosyasını yazmaya başlayabiliriz. Biraz uzun bir kod dosyası ama sabırla yazıp, yorum satırlarını da okuyarak neler yaptığımızı anlamaya çalışmakta yarar var.</div>
<div>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"strings"
"go.mongodb.org/mongo-driver/bson/primitive"
playerpb "gRPC-sample/proto"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
/* proto'dan otomatik üretilen player.pb.go içerisindeki RegisterPlayerServiceServer metoduna bir bakın.
Pointer olarak gelen grpc server nesnesi ikinci parametre olarak gelen tipi register etmek için kullanılır.
Bir nevi interface üzerinden enjekte işlemi yaptığımızı düşünebilir miyiz?
*/
type PlayerServiceServer struct{}
var db *mongo.Client
var playerCollection *mongo.Collection
var mongoContext context.Context
func main() {
// TCP üzerinden 5555 nolu portu dinleyecek olan nesne oluşturuluyor
server, err := net.Listen("tcp", ":5555")
// Olası bir hata durumunu kontrol ediyoruz
if err != nil {
fmt.Printf("5555 dinlenemiyor: %v", err)
}
// gPRC sunucusu için kayıt(register) işlemleri
grpcOptions := []grpc.ServerOption{}
// yeni bir grpc server oluşturulur
grpcServer := grpc.NewServer(grpcOptions...)
// Bir PlayerService tipi oluşturulur
playerServiceType := &PlayerServiceServer{}
// servis sunucu ile birlikte kayıt edilir
playerpb.RegisterPlayerServiceServer(grpcServer, playerServiceType)
// mongoDB bağlantı işlemleri
fmt.Println("MongoDB sunucusuna bağlanılıyor")
mongoContext = context.Background()
// bağlantı deneniyor
db, err = mongo.Connect(mongoContext, options.Client().ApplyURI("mongodb://localhost:27017"))
// olası bir bağlantı hatası varsa
if err != nil {
fmt.Println(err)
}
// Klasik ping metodunu çağırıyoruz
err = db.Ping(mongoContext, nil)
if err != nil {
fmt.Println(err)
} else {
// çağrı başarılı olursa bağlandık demektir
fmt.Println("MongoDB ile bağlantı sağlandı")
}
// nba isimli veritabanındaki player koleksiyonuna ait bir nesne örnekliyoruz
// veritabanı ve koleksiyon yoksa oluşturulacaktır
playerCollection = db.Database("nba").Collection("player")
// gRPC sunucusunu aktif olan TCP sunucusu içerisinde bir child routine olarak başlatıyoruz
go func() {
if err := grpcServer.Serve(server); err != nil {
fmt.Println(err)
}
}()
fmt.Println("Sunucu 5555 nolu porttan gPRC tabanlı iletişime hazır.\nDurdurmak için CTRL+C.")
// CTRL+C ile başlayan kapatma operasyonu
cnl := make(chan os.Signal) // işletim sisteminde sinyal alabilmek için bir kanal oluşturduk
signal.Notify(cnl, os.Interrupt) // CTRL+C mesajı gelene kadar ana rutin açık kalacak
<-cnl
fmt.Println("Sunucu kapatılıyor...")
grpcServer.Stop() // gRPC sunucusunu durdur
server.Close() // TCP dinleyicisini kapat
fmt.Println("GoodBye Crow")
}
/* Protobuf mesajlarında taşınan serileşmiş içeriği nesnel olarak ele alacağımı struct */
type Player struct {
ID primitive.ObjectID `bson:"_id,omitempty"` // MongoDB tarafındaki ObjectId değerini taşır
PlayerID string `bson:"player_id"`
Fullname string `bson:"fullname"`
Position string `bson:"position"`
Bio string `bson:"bio"`
}
/* PlayerServiceServer'ın uygulanması gereken metodlarını. Yani servis sözleşmesinin tüm operasyonları
*/
// Yeni bir oyuncu eklemek için kullanacağımız fonksiyon
func (srv *PlayerServiceServer) AddPlayer(ctx context.Context, req *playerpb.AddPlayerReq) (*playerpb.AddPlayerRes, error) {
payload := req.GetPlyr() // GetPlyr (GetPlayer değil o servis metodumuz) fonksiyonu ile request üzerinden gelen player içeriği çekilir
// İçerik ile gelen alan değerleri player struct nesnesini oluşturmak için kullanılır
player := Player{
PlayerID: payload.GetPlayerId(),
Fullname: payload.GetFullname(),
Position: payload.GetPosition(),
Bio: payload.GetBio(),
}
// player nesnesi mongodb veritabanındaki koleksiyona kayıt edilir
result, err := playerCollection.InsertOne(mongoContext, player)
// bir problem oluştuysa
if err != nil {
// gRPC hatası döndürülür
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Bir hata oluştu : %v", err),
)
}
// Hata oluşmadıysa koleksiyona eklenen yeni doküman
// üretilen ObjectID değeri de atanarak geri döndürülür
objectID := result.InsertedID.(primitive.ObjectID)
payload.Id = objectID.Hex()
return &playerpb.AddPlayerRes{Plyr: payload}, nil
}
func (srv *PlayerServiceServer) EditPlayer(ctx context.Context, req *playerpb.EditPlayerReq) (*playerpb.EditPlayerRes, error) {
return nil, nil
}
func (srv *PlayerServiceServer) RemovePlayer(ctx context.Context, req *playerpb.RemovePlayerReq) (*playerpb.RemovePlayerRes, error) {
// önce silinmek istenen playerId bilgisi alınır
id := strings.Trim(req.GetPlayerId(), "\t \n")
fmt.Println(id)
// DeleteOne metodu ile silme operasyonu gerçekleştirilir
_, err := playerCollection.DeleteOne(ctx, bson.M{"player_id": id})
// hata kontrolü yapılıyor
if err != nil {
return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Silinmek istenen oyuncu bulunamadı. %s", err))
}
// hata yoksa işlemin başarılı olduğuna dair sonuç dönülür
return &playerpb.RemovePlayerRes{
Removed: true,
}, nil
}
// MongoDB'deki ID bazlı olarak oyuncu verisi döndüren metodumuz
func (srv *PlayerServiceServer) GetPlayer(ctx context.Context, req *playerpb.GetPlayerReq) (*playerpb.GetPlayerRes, error) {
// request ile gelen player_id bilgisini alıyoruz
// Trim işlemi önemli. İstemci terminalden değer girdiğinde alt satıra geçme işlemi söz konusu.
// Veri bu şekilde gelirse kayıt bulunamaz. Dolayısıyla bir Trim işlemi yapıyoruz
id := strings.Trim(req.GetPlayerId(), "\t \n")
// bson.M metoduna ilgili sorguyu ekleyerek oyuncuyu koleksiyonda arıyoruz
result := playerCollection.FindOne(ctx, bson.M{"player_id": id})
player := Player{}
// bulunan oyuncu decode metodu ile ters serileştirilip player değişkenine alınır
if err := result.Decode(&player); err != nil {
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Sanırım aranan oyuncu bulunamadı %v", err))
}
// Decode işlemi başarılı olur ve koleksiyondan bulunan içerik player isimli değişkene ters serileşebilirse
// artık dönecek response nesne içeriğini hazırlayabiliriz
res := &playerpb.GetPlayerRes{
Plyr: &playerpb.Player{
Id: player.ID.Hex(),
PlayerId: player.PlayerID,
Fullname: player.Fullname,
Position: player.Position,
Bio: player.Bio,
},
}
return res, nil
}
// Tüm oyuncu listesini stream olarak dönen metod
func (srv *PlayerServiceServer) GetPlayerList(req *playerpb.GetPlayerListReq, stream playerpb.PlayerService_GetPlayerListServer) error {
currentPlayer := &Player{}
// Find metodu veri üzerinden hareket edebileceğimiz bir Cursor nesnesi döndürür
// bu cursor nesnesi sayesinde istemciye tüm oyuncu listesini bir seferde göndermek yerine
// birer birer gönderme şansına sahip olacağız
// Bu nedenle sunucu bazlı bir streamin stratejimiz var
cursor, err := playerCollection.Find(context.Background(), bson.M{})
if err != nil {
return status.Errorf(codes.Internal, fmt.Sprint("Bilinmeyen hata oluştu"))
}
// metod işleyişini tamamladığında cursor nesnesini kapatacak çağrıyı tanımlıyoruz
defer cursor.Close(context.Background())
// iterasyona başlanır ve Next true döndüğü sürece devam eder
// yani okunacak mongodb dokümana kalmayana dek
for cursor.Next(context.Background()) {
// cursor verisini currentPlayer nesnesine açıyoruz
cursor.Decode(currentPlayer)
// istemciye mongodb'den gelen güncel oyuncu bilgisinden yararlanarak cevap dönüyoruz
stream.Send(&playerpb.GetPlayerListRes{
Plyr: &playerpb.Player{
Id: currentPlayer.ID.Hex(),
PlayerId: currentPlayer.PlayerID,
Fullname: currentPlayer.Fullname,
Position: currentPlayer.Position,
Bio: currentPlayer.Bio,
},
})
}
return nil
}</pre>
Sunucu tarafındaki kodlama tamamlandıktan sonra istemci tarafı için clientapp altında tester.go isimli bir başka dosya oluşturarak ilerleyelim. Burada komut satırından temel CRUD operasyonlarını icra edeceğiz. Yeni bir oyuncunun eklenmesi, bir oyuncu bilgisinin çekilmesi, tüm oyuncuların listesinin alınması vb</div>
<div>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"strings"
playerpb "gRPC-sample/proto"
"google.golang.org/grpc"
)
var client playerpb.PlayerServiceClient
var reqOptions grpc.DialOption
func main() {
// HTTPS ayarları ile uğraşmak istemedim
reqOptions = grpc.WithInsecure()
// gRPC servisi ile el sıkışmaya çalışıyoruz
connection, err := grpc.Dial("localhost:5555", reqOptions)
if err != nil {
fmt.Println(err)
return
}
// proxy nesnesini ilgili bağlantıyı kullanacak şekilde örnekliyoruz
client = playerpb.NewPlayerServiceClient(connection)
// Oyuncu ekleyelim
insertPlayer()
// tüm oyuncu listesini çekelim
getAllPlayerList()
// sembolik olarak ID bazlı 3 oyuncu aratalım
for i := 0; i < 3; i++ {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Oyuncu IDsini gir.")
playerID, _ := reader.ReadString('\n')
getByPlayerID(playerID)
}
// Silme operasyonunu deniyoruz
reader := bufio.NewReader(os.Stdin)
fmt.Println("Silmek istediğiniz oyuncunun IDsini girin.")
playerID, _ := reader.ReadString('\n')
removePlayerByID(playerID)
// tüm oyuncu listesini çekelim
getAllPlayerList()
}
func insertPlayer() {
// Yeni oyuncu eklenmesi için deneme kodu
// Veri ihlalleri örneğin basitliği açısından göz ardı edilmiştir
reader := bufio.NewReader(os.Stdin)
fmt.Println("Yeni oyuncu girişi")
fmt.Println("Id->")
id, _ := reader.ReadString('\n')
id = strings.Replace(id, "\n", "", -1)
fmt.Println("Adı->")
fullname, _ := reader.ReadString('\n')
fullname = strings.Replace(fullname, "\n", "", -1)
fmt.Println("Pozisyon->")
position, _ := reader.ReadString('\n')
position = strings.Replace(position, "\n", "", -1)
fmt.Println("Kısa biografisi->")
bio, _ := reader.ReadString('\n')
bio = strings.Replace(bio, "\n", "", -1)
// protobuf dosyasındaki şemayı kullanarak örnek bir oyuncu nesnesi örnekliyoruz
newPlayer := &playerpb.Player{
PlayerId: id,
Fullname: fullname,
Position: position,
Bio: bio,
}
// servisin AddPlayer metodunu o anki context üzerinden çalıştırıp
// request payload içerisinde yeni oluşturduğumuz nesneyi gönderiyoruz
res, err := client.AddPlayer(
context.TODO(),
&playerpb.AddPlayerReq{
Plyr: newPlayer,
},
)
if err != nil {
fmt.Println(err)
return
}
// Eğer bir hata oluşmamışsa MongoDB tarafından üretilen ID değerini ekranda görmemiz lazım
fmt.Printf("%s ile yeni oyuncu eklendi \n", res.Plyr.Id)
}
// Tüm oyuncu listesini çektiğimiz metod
func getAllPlayerList() {
// önce request oluşturulur
req := &playerpb.GetPlayerListReq{}
// proxy nesnesi üzerinden servis metodu çağrılır
s, err := client.GetPlayerList(context.Background(), req)
if err != nil {
fmt.Println(err)
return
}
// sunucu tarafından stream bazlı dönüş söz konusu
// yani kaç tane oyuncu varsa herbirisi için sunucudan istemciye
// cevap dönecek
for {
res, err := s.Recv() // Recv metodu player.pb.go içerisine otomatik üretilmiştir. İnceleyin ;)
if err != io.EOF { // döngü sonlanmadığı sürece gelen cevaptaki oyuncu bilgisini ekrana yazdırır
fmt.Printf("[%s] %s - %s \n\n", res.Plyr.PlayerId, res.Plyr.Fullname, res.Plyr.Bio)
} else {
break
}
}
}
// Oyuncuyu PlayerID değerinden bulan metodumuz
func getByPlayerID(playerID string) {
// parametre olarak gelen playerID değerinden bir request oluşturulur
req := &playerpb.GetPlayerReq{
PlayerId: playerID,
}
// GetPlayer servis metoduna talep gönderilir
res, err := client.GetPlayer(context.Background(), req)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(res.Plyr.Fullname)
}
// Oyuncu silme fonksiyonumuz
func removePlayerByID(playerID string) {
// RemovePlayer servis çağrısı için gerekli Request tipi hazırlanır
req := &playerpb.RemovePlayerReq{
PlayerId: playerID,
}
// servisi çağrısı yapılıp sonucu kontrol edilir
_, err := client.RemovePlayer(context.Background(), req)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Oyuncu silindi")
}
</pre>
Piuvvv!!! Uzun bir yol oldu. Öyleyse çalışma zamanı sonuçlarımıza bakalım mı?</div>
<h2>Çalışma Zamanı</h2>
<div>İlk gün çalışmasının meyveleri pek fena değil. server ve client tarafa ait go dosyalarını kendi klasörlerinde aşağıdaki terminal komutları ile derledikten sonra</div>
<div>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">go build main.go
go build tester.go</pre>
önce sunucu ardından istemci programlarını çalıştırıp kodlaması ilk önce biten AddPlayer fonksiyonunu deneme şansı buldum. Birkaç oyuncu verisini girdikten sonra mongodb container'ına ait shell'e bağlanıp gerçekten de yeni dokümanların player koleksiyonuna eklenip eklenmediğine baktım. Sonuç tebessüm ettiriciydi :) İstemci uygulama gRPC üzerinden sunucuya mesaj göndermiş, sunucuya gelen içerik docker container üzerinde duran mongodb veritabanına yazılmıştı.</div>
<div> </div>
<div><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_4.png" alt="" /></div>
<div> </div>
<div>İkinci gün tüm oyuncu listesini gRPC üzerinden istemciye döndüren süreci yazmaya çalıştım. İlk başta yaptığım bir hata nedeniyle epey vakit kaybettim. GetPlayerList metodunu protobuffer dosyasında stream döndürecek şekilde tasarlamamıştım. Büyük bir veri kümesini filtresiz çektiğimizde bu ağ trafiğinin sağlıklı çalışması açısından sorun olabilir. Oyuncuları sunucudan istemciye doğru bir stream üzerinden tek tek göndermek çok daha mantıklı<em>(Burada REST ile gRPC arasındaki farkları hatırlayalım)</em> Sonunda servis sözleşmesini değiştirip gerekli düzenlemeleri yaptıktan sonra aşağıdaki ekran görüntüsünde yer alan mutlu sona ulaşmayı başardım.</div>
<div> </div>
<div><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_5.png" alt="" /></div>
<div> </div>
<div>Devam eden gün bir öncekine göre daha zorlu geçti. FindOne metodunu player_id değerine göre çalıştırmayı bir türlü başaramadım. Neredeyse 4 pomodoro periyodu uğraştım. Hatta pomodoro süreci bittikten sonra farkında olmadan saatlerce bilgisayar başında kaldım. Sorunu araştırırken vakit nasıl geçti anlamamışım. Sonuçta işe 3 saatlik uykuyla gittim. Ertesi gün Ahch-To'nun tuşuna bile basmadım. Bir günlük ara, problemi çözmem için beni sakinleştirmeye yeterdi. Nihayetinde sorunu buldum. İstemci aradığı ID değerini girip sunucuya çağrı yaptığında, servis metoduna gelen ID bilgisinin sonunda boşluk ve alt satıra geçme karakterleri de geliyordu. Trim fonksiyonu ile bu durumun oluşmasını engelledikten sonra silme operasyonunu da işin içerisine dahil ettim ve güncelleme operasyonu hariç komple bir test yaptım. Sonuçlar ekran görüntüsünde olduğu gibi tatmin ediciydi.</div>
<div> </div>
<div><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_6.png" alt="" /></div>
<div>Silme operasyonuna ilişkin çalışmaya ait örnek bir ekran görüntüsü de aşağıdaki gibi.</div>
<div><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/11/02/screenshot_7.png" alt="" /></div>
<h2>Neler Öğrendim?</h2>
<div>Elbette SkyNet'te geçirdiğim bugünün de bana öğrettiği bir sürü şey oldu. Bunları aşağıda yer alan maddelerle ifade etmeye çalıştım.</div>
<ul>
<li>Bir protobuf dosyası nasıl hazırlanır ve Go tarafında kullanılabilmesi için nasıl derlenir,</li>
<li>Go tarafından MongoDB ile nasıl haberleşilir,</li>
<li>MongoDB docker container'ına ait shell üstünde nasıl çalışılır,</li>
<li>Temel mongodb komutları nelerdir,</li>
<li>Sunucudan istemciye stream açarak tek tek mongo db dokümanı nasıl döndürülür<em>(main.go'daki GetPlayerList metoduna bakın)</em></li>
</ul>
<h2>Eksikliği Hissedilen Konular</h2>
<p>Her ne kadar pomodoro tekniği ile çalışmalarımı olabildiğince verimli hale getirsem de ister istemez yaşlı zihnim yoruluyor. Dolayısıyla şunları da yapabilsem iyi olurdu dediğim şeyler var. Bunları da şu iki madde ile sıralayabilirim.</p>
<ul>
<li>İstemci tarafını Go tabanlı bir web client olarak geliştirmeyi deneyebiliriz. Terminalden hallice daha iyidir. En azından çalışma sırasında yaşadığım Trim ihlali oluşmaz.</li>
<li>Bir çok sunucu metodunda hata kontrolü var ancak bunların çalışıp çalışmadığı test etmek gerekiyor. Yani Code Coverage değerimizi neredeyse 0. Yazıyla sıfır :) Bir Go uygulamasındaki fonksiyonlar için Unit Test'ler nasıl yazılır öğrenmem lazım.</li>
</ul>
<h2>Görev Listeniz</h2>
<p>Ve tabii kabul ederseniz sizin için iki güzel görevim var :)</p>
<ul>
<li>Select * from players where fullname like 'A%' gibi bir sorguya karşılık gelecek mongodb fonksiyonunu geliştirip uygulamaya ekleyin.</li>
<li>Güncelleme fonksiyonunu tamamlayın.</li>
</ul>
<p>Böylece geldik SkyNet'te bir günün daha sonuna. Sonraki çalışmada Wails paketini kullanarak Go ile yazılmış bir masaüstü programı geliştirmek niyetindeyim. O zaman dek hepinize mutlu günler dilerim.</p>2019-12-18T08:00:00+00:00go programming languagemongodbdockergRPCpomodororestapiprotobufcontainerbrewprotojsonbsenyurtElimizdeki malzemeleri sayalım. MongoDB için bir docker imajı, gRPC ve GoLang. Bu üçünü kullanarak CRUD operasyonlarını icra eden basit bir uygulama geliştirmek niyetindeyim. Bir önceki öğretide Redis docker container'dan yararlanmıştım. Ahch-to sistemini kirletmemek adına MongoDB için de benzer şekilde hareket edeceğim. Açıkçası GoLang bilgim epey paslanmış durumda ve sistemde yüklü olup olmadığını dahi bilmiyorum.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=f41bd80e-3d32-4544-a97d-cda7bf00e3c02https://www.buraksenyurt.com/trackback.axd?id=f41bd80e-3d32-4544-a97d-cda7bf00e3c0https://www.buraksenyurt.com/post/mongodb-ile-bir-go-uygulamasini-konusturmak#commenthttps://www.buraksenyurt.com/syndication.axd?post=f41bd80e-3d32-4544-a97d-cda7bf00e3c0https://www.buraksenyurt.com/post/go-web-sunucusunu-docker-uzerinden-yayinlamakGo Web Sunucusunu Docker Üzerinden Yayınlamak2017-11-15T10:00:00+00:00bsenyurt<p><img style="float: right;" src="https://www.buraksenyurt.com/image.axd?picture=/2017/11/go_docker5.gif" alt="" />Merhaba Arkadaşlar,</p>
<p>Gondor'da bir şeyler araştırmak için harika bir zaman. Çünkü elimdeki işler bitti. Böyle vakitleri kendi araştırmalarıma ayırmak hoşuma gidiyor, kim ne derse desin. Yeni gözdem Linux makinem de<em>(Gondor)</em> önümde durduğuna göre kısa bir süre onun üzerinde çalışabileceğimi düşünüyorum.</p>
<p>Aklıma gelen ilk şey ise, Go diliyle yazılmış ilkel bir web sunucusunu Docker üzerinden kullanabilmek<em>.</em> Önce web sunucusunu geliştirmek, başarılı bir şekilde çalıştığından emin olmak, sonrasında bir Docker imajı hazırlamak lazım. Ardından oluşturulan imajdan yararlanarak bir Container başlatıp web sunucusunun bu taşıyıcı örneği üzerinden çalışıp çalışmadığını test etmek senaryonun tamamlanması açısından yeterli. Tahminlerime göre 15 dakikayı aşmayacak bir iş gibi duruyor. Haydi başlayalım.</p>
<p>Gondor'da açtığım SimpleWebServer klasörüne aşağıdaki kod parçasını içeren main.go isimli bir dosya ekleyerek ilerliyorum. Tabii buradaki ortamda GOPATH tanımlamalarını değiştirmiştim. $home\goprojects altında konuşlandırıyorum <em>(Geliştirici arabirimi olarak Visual Studio Code' tan faydalanıyorum. Her zaman ki gibi çok keyifli bir geliştirici deneyimi sunuyor. Size de tavsiye ederim)</em></p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import(
"fmt"
"net/http"
"runtime"
"math/rand"
"time"
)
func indexHandler(w http.ResponseWriter,r *http.Request){
t:=time.Now()
w.Header().Set("Content-Type","text/html; charset=utf-8")
fmt.Fprintf(w,"Gondor time : <b>(%s)</b><br/>We are running on <b>%s</b> with an <b>%s</b> CPU<br/><i>Your lucky number</i> <b>%d</b>",t.Format(time.RFC1123),runtime.GOOS,runtime.GOARCH,rand.Intn(100))
}
func main(){
http.HandleFunc("/",indexHandler)
fmt.Println("listening...")
http.ListenAndServe(":8087",nil)
}</pre>
<p>Uzun zamandır Go ile kod yazmıyordum. Özlediğimi ifade edebilirim. Özellikle de kurallarını ve basitliğini. Ana paketteki programın başlangıç noktası olan main içerisinde HandleFunc isimli fonksiyondan yararlanarak root adrese gelecek olan talepleri indexHandler isimli operasyona yönlendiriyoruz. indexHandler içerisinde ise çok basit bir HTML içeriği bastırmaktayız. Elle tutulur bir şeyler olması açısından güncel zaman bilgisini, işletim sistemini, işlemcinin türevini yazdırdıktan sonra 0 ile 100 arasında üretilecek rastgele bir sayı da basıyoruz. İçeriğin HTML tipinden olacağını Header'a ait Set fonksiyonu ile belirtmekteyiz. Böylece istemciye ulaşacak paketin html olarak yorumlanılması gerektiğini de söylemiş oluyoruz. main içerisinde yer alan ListenAndServe fonksiyonu da 8087<em>(istediğiniz bir portu kullanabilirsiniz tabii)</em> nolu porttan yayın yapılmasını sağlamakta. </p>
<p>İlk olarak kodun bu şekilde çalışıp çalışmadığını test etmek lazım. Terminalden </p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">go run main.go</pre>
<p>komutunu vererek bu denemeyi yaşayabiliriz. Aşağıdaki ekran görüntüsünde örnek bir çalışma zamanına yer veriliyor. Ben bu görüntüyü aldığımda yine gülümsedim hafifçe.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/11/godocker_1.gif" alt="" /></p>
<p>Hedefimiz şu. Bu uygulamayı başlatıldığı zaman ayağa kaldıracak bir Docker imajı oluşturmak. Bunun yolu bildiğiniz gibi ilgili komutları içerecek bir Dockerfile oluşturmaktan geçiyor. main.go ile aynı lokasyona aşağıdaki içeriğe sahip docker dosyasını ekleyerek ilerleyebiliriz.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">FROM golang
ADD . /go/src/GoWebServer
RUN go install GoWebServer
ENTRYPOINT [ "/go/bin/GoWebServer" ]
EXPOSE 8087</pre>
<p>Öncelikle resmi golang imajından feyz aldığımızı belirtelim. İlk satırda bunu belirtmekteyiz. Sonrasında kaynak kodun tamamını oluşturulan imaj içerisindeki /go/src/GoWebServer adresine taşıyoruz. Tahmin edileceği üzere golang imajındaki GOROOT ve GOPATH ortam ayarlamalarına uyan taşımalar yapılmakta. Bu path tanımlamaları ata imajda hazırlanmış. GOPATH tanımına göre kaynak kodları go/src altına atmamız gerekiyor. Sonrasında GoWebServer içeriğini sisteme yüklüyoruz<em>(Go Deployment)</em> RUN ifadesinden sonra gelen komut bu işi yapmakta. Container başlatıldığında giriş noktası olarak kurulumun gerçekleştiği go/bin/GoWebServer klasörünü işaret ediyoruz. Son olarak Container'ı localhost:8087 portu üzerinden yayına açıyoruz.</p>
<p>Dosya hazırlandıktan sonra imajın oluşturulmasına başlayabiliriz. Bu tipik olarak Docker'ın Build operasyonu. Terminalden aşağıdaki komutu kullanarak inşayı başlatabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">sudo docker build -t go_rnd_server .</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/11/godocker_22.gif" alt="" /></p>
<p>go_rnd_server isimli bir imaj oluşturduk. Eğer indirilmesi gereken içerikler varsa sisteme yüklenmesi için bir süre beklemek gerekebilir. Herhangibir hata alınmadıysa oluşturulan imajı kullanarak yeni bir Container başlatabiliriz. Bunun için docker'ın run komutundan yararlanmak gerekiyor. -p den sonra gelen adres ile yayınlamanın hangi adresten yapılacağını da belirtmiş oluyoruz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">sudo docker run -p 8087:8087 go_rnd_server</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/11/go_docker33.gif" alt="" /></p>
<p>Ekran görüntüsünden de görüleceği gibi artık docker üzerinde konuşlandırdığımız Web sunucusuna gidebiliyoruz<em>(Biraz daha gülümsüyorum)</em> Örneği genişletmek tabii ki sizin elinizde. Burada Go ile yazılmış bir REST servis de sunulabilir. Hatta veri için Redis gibi bir yapı kullanılarak senaryo daha da heyecanlı hale getirilebilir<em>(En sevdiğiniz filmdeki sözlerin içeren bir veri kümesinden kullanıcıya rastgele sözler yolladığınız bir örnek üzerinde çalışabilirsiniz) </em>Bunlara ek olarak söz konusu Container'lardan bir kaçının farklı port'lardan başlatılması da denenebilir. Bu mümkün olabilir mi şimdilik bilmiyorum ama denemek de istiyorum. Özellikle ortak veri kullanımları veya verinin tüm Container'lar için eşleştirilmesi gibi epik senaryoları kafamda şekillendirmekte henüz zorlanıyorum.</p>
<p>Makaleme son vermeden önce çalışmakta olan Container'ı nasıl durduracağımızı da söyleyeyim. Eğer bir Deploy işlemi gerçekleştireceksek bunun öncesinde ilgili Container örneğinin durdurulması önerilmiş. İlk olarak var olan Container örneklerini görelim. Terminalden vereceğimiz aşağıdaki komutu bunu sağlayacaktır.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">sudo docker ps -a</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/11/go_docker4.gif" alt="" /></p>
<p>Çalışmakta olan bir Container örneğini durdurmak için de ID değerini kullanabiliriz. Aşağıdaki gibi.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">sudo docker stop fb3936d9f595</pre>
<p>Böylece geldik kısa süreli faydası olan bir öğle arasının daha sonuna. Artık işin sırrı öğrenildi diyebilirim. Hakim olduğunuz bir dille basit bir Web sunucusu veya REST servisi yazıp Docker ile sunmayı deneyebilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2017-11-15T10:00:00+00:00golanggo programming languagedockercontainerweb hostingbsenyurtUzun zamandır Go ile kod yazmıyordum. Özlediğimi ifade edebilirim. Özellikle de kurallarını ve basitliğini. Ana paketteki programın başlangıç noktası olan main içerisinde HandleFunc isimli fonksiyondan yararlanarak root adrese gelecek olan talepleri indexHandler isimli operasyona yönlendiriyoruz. indexHandler içerisinde ise çok basit bir HTML içeriği bastırmaktayız. Elle tutulur bir şeyler olması açısından güncel zaman bilgisini, işletim sistemini, işlemcinin türevini yazdırdıktan sonra 0 ile 100 arasında üretilecek rastgele bir sayı da basıyoruz.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=eb80b38a-b1bf-4996-9e92-305e895eae841https://www.buraksenyurt.com/trackback.axd?id=eb80b38a-b1bf-4996-9e92-305e895eae84https://www.buraksenyurt.com/post/go-web-sunucusunu-docker-uzerinden-yayinlamak#commenthttps://www.buraksenyurt.com/syndication.axd?post=eb80b38a-b1bf-4996-9e92-305e895eae84https://www.buraksenyurt.com/post/golang-redis-ile-anlasmakGoLang - Redis ile Anlaşmak2017-08-03T21:16:00+00:00bsenyurt<p><img style="float: right;" src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/goredis_2.gif" alt="" />Merhaba Arkadaşlar,</p>
<p>Bir haziran gecesiydi. Dışarıda hava nemli ve sıcaktı. Bir süre önce başlayan yağmurun sesi çalışma odamın pencersinden kulaklarıma tatlı tatlı geliyordu. Biraz da toprak kokusu vardı tabii. Evde el ayak çekilmiş sakin bir ortam oluşmuştu. Bol kafein dolu bardağım elimde internetten bir şeyler okuyordum. İnsanlar çıldırmıştı. Javascript Framework'ler, yapay zeka'lar, react'ler, cordova'lar, .net core'lar, sanayi 4.0'lar, tesla'lar ve daha niceleri. Eskisinden daha hızlı bir şekilde geride kaldığımı hissediyordum. Sanırım sonum örümcek adamın amcası gibi evde bozuk ampülü tamir edip gazetede iş arayan ama bulamayan biri gibi olacaktı. Ama direniyordum. Önce Ruby, sonra Pyhton ve derken GO. Amatör seviyede başlamış biraz ilerlemiştim. Kendime bir çalışma döngüsü kurmuştum. Bir süre Ruby bakıyor, sonra Pyhton bakıyor, sonra GO bakıyor sonra bu döngüyü tekrar başa sarıyordum. GO'nun ikinci iterasyonundaydım.</p>
<p>Bu sefer elimde oldukça güzel de bir Türkçe kaynak vardı<em>(Murat Özalp'in Go Programlama isimli kitabını şiddetle tavsiye ederim)</em> Kitabın son sayfalarına gelmiştim. Döngünün bir sonraki adımına geçmeden önce biraz daha uygulama yapmam gerekiyordu. Nitekim dile hakim olabilmek için bol bol kod yazmam şarttı. Mesela GoLangWeekly' de yayımlanan başlıklara göz gezdirdiğimde çoğunu anlamakta güçlük çekiyordum. Bu yüzden seçmece ilerlemek durumundaydım. Aslında programlama dilini ufak ufak kavramaya başlamıştım ama örnek senaryolar işleterek ilerlemem gerektiğini de biliyordum. Gelişebilmem için bu şarttı. Bu karman çorman düşünceler altında devam ederken aklıma bir pratik geldi. Eskiden .Net tarafında kullandığım NoSQL sistemlerinden olan Redis ile ilgili bir şeyler yapmak istiyordum.</p>
<blockquote>
<p>2014 yılında onu kısaca incelemeye çalışmış ve <a href="https://www.buraksenyurt.com/post/NoSQL-Maceralarc4b1e28093Redis-ile-Hello-World" target="_blank">bir şeyler karalamıştım</a>. Hatta Redis'in genel özelliklerini oradan okuyarak hatırlamaya çalıştım. Bu yazı için kısaca özetlemek gerekirse bellekte çalışan, key-value tipinde ve dağıtık yapıda sunulabilen bir NoSQL sistemi olduğunu ifade edebiliriz. Redis'in key-value tipinde bir veri tabanı olması bizi yanıltmamalıdır. key'ler string olsa da value olarak kullanabileceğimiz beş temel tip bulunur. string, hash, list, set ve sortedSet. Dolayısıyla oldukça geniş bir veri yapısını kullanma şansımız vardır.</p>
</blockquote>
<p>Zaman hızla geçmiş bir çok şey de değişmişti tabii. Bir süredir gözde dillerimden birisi olan Go'da Redis'i nasıl kullanabileceğimi merak ediyordum. O gece Windows makinesinde çalışmaktaydım. Aradan geçen zamana rağmen Redis'in Windows sürümü halen çalışmaktaydı. Yeni Redis versiyonlarına göre yeni sürümleri de yayınlanıyordu. Dolayısıyla siz de Windows sürümündeyseniz <a href="https://github.com/MSOpenTech/redis/releases" target="_blank">şu adresten</a> uygun MSI ile yüklemeyi yapabilir ve yazının kalanında bana eşlik edebilirsiniz.</p>
<h1>Komut Satırında Kısa Bir Tur</h1>
<p>Yükleme işlemi sonrası hemen komut satırına geçtim ve redis-server.exe' yi kurulumun yapıldığı lokasyondan çalıştırdım. Sonra bir kaç deneme yapmak için redis-client ile açılan kısma geçtim. Redis varsayılan olarak yerel makinede 6379 numaralı porttan hizmet veren bir servis. Tabii farklı node'lar söz konusu olursa farklı port'lar ile de haberleşebilmemiz mümkün. Örneğimiz şimdilik tek bir sunucu örneği üzerinden çalışıyor.</p>
<p>Saatler ilerlerken konunun verdiği heyecanla komut satırından bir kaç deneme yapmayı da ihmal etmedim.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/goredis_1.gif" alt="" /></p>
<p>İlk olarak redis ile ping-pong oynadım :) Siz ping yazdığınızda O da PONG diyorsa bu konuşabildiğiniz anlamına gelir. Ardından ilk iş players:reksar isimli bir key oluşturmak oldu. Değeri ise JSON formatında bir içerikten ibaretti. get komutunu kullanarak belleğe atılan bu key içeriğini okuyabiliriz.</p>
<p>Sonrasında hmset ile bir hash üretmeye çalıştım. hmset ile bir key için n sayıda değer içerecek alanlar(fields) tanımlayabiliriz. language:go isimli key bu şekilde yazıldı. Oluştururken bir field bir value, bir field bir value şeklinde ilerlemek gerekiyor. Daha sonra hmget ile language:go içeriğini almaya çalıştım. Ancak ilk denemede hata yaptım. Nitekim bu komut ile bir hash içerisindeki belli bir alanın değerini almaktayız. type alanının değerini okuduktan sonra tüm alanların içeriğini hgetall komutu ile elde etmeyi başardım. Dilersek sadece key değerlerini de yakalayabiliriz ki hkeys bu noktada devreye girmekte. </p>
<p>İlerleyen satırlarda basit bir liste oluşturup ona elemanlar eklemeyi ve tüm içeriği ekrana yazdırmayı denedim. Bu amaçla lpush ve lrange isimli komutlardan yararlandım. Bu şekilde komut satırından çalışmaya da devam edilebilirdi tabii ama hedefim bunu Go ile gerçekleştirmekti.</p>
<h1>GoLang Zamanı</h1>
<p>Diğer platformlarda olduğu gibi Redis'i GoLang ile kullanmak için harici bir paketten destek almam işleri kolaylaştırıyor. Aslında bu şart değil. Sonuçta servis bazlı bir veritabanı motoru söz konusu ama şimdilik paket ile ilerlemek benim için daha iyi. Bir kaç araştırma ve blog yazısından sonra <a href="https://github.com/mediocregopher/radix.v2" target="_blank">şu adresten yayınlanan bir go paketi</a> buldum. LiteIDE'nin Get seçeneği ile ya da komut satırından ilgili paketi kolaylıkla sistemimize alabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">go get github.com/mediocregopher/radix.v2</pre>
<h2>Önce Basit Bir String Ekleyelim</h2>
<p>Gelelim örnek kod parçalarına. İlk olarak basit string türünden bir veri eklemeye çalıştım. Value olarak da JSON içeriği kullanmaya karar verdim.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import (
"fmt"
"github.com/mediocregopher/radix.v2/redis"
)
func main() {
AddLudwig()
}
func AddLudwig() {
conn, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println(err.Error())
} else {
defer conn.Close()
pong := conn.Cmd("ping")
fmt.Println(pong.String())
response := conn.Cmd("set", "players:ludwig", "{\"nick\":ludwig,\"genre\":classic,\"SongCount\":98}")
if response.Err != nil {
fmt.Println(response.Err)
}
fmt.Println(response.String())
}
}</pre>
<p>Fonksiyon redis tipinin Dial metodu ile başlıyor. TCP protokolü ile localhost üzerindeki 6379 nolu porta bağlanacağımızı ifade ediyoruz. Yani Redis sunucusuna. Eğer bağlanabiliyorsak<em>(ki err nesnesi nil ise bağlanıyoruz diyebiliriz)</em> önce ping pong oynuyor ve sonrasında players:ludwig isimli bir key gönderiyoruz. Değer olarak da JSON formatında bir içerik söz konusu. Ludwig'in takma adını, bestelediği şarkı türünü ve toplam parça sayısını tutan saçma bir verimiz var. Bu kodda en kritik nokta az önce terminalden yazdığımız redis komutlarının Cmd metodunda kullanılması. İlk çağrıda ping diğerinde ise set komutunu göndermekteyiz. defer ettiğimiz Close metodu fonksiyondan çıkarken redis bağlantısını kapatacak. Çalışma zamanı sonuçlarını aşağıda görebilirsiniz. Koddan eklediğimiz veriyi redis komut satırından da elde edebildik.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/goredis_3.gif" alt="" /></p>
<h2>Birde Hash Üretip Okuyalım</h2>
<p>Kodları biraz daha ilerletmeye çalıştım. Acaba bir Hash nasıl üretilebilirdi ve hatta alanlarının değerlerini kod tarafından nasıl okuyabilirdim? Aşağıdaki gibi bir fonksiyon işime yarayacaktı.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">func AddAndReadHash() {
conn, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println(err.Error())
} else {
defer conn.Close()
response := conn.Cmd("HMSET", "card:93", "nickName", "murlock", "greetings", "I'am ready, I'am not ready", "price", 5, "attack", 4, "defense", 4, "owner", "shammon")
if response.Err != nil {
fmt.Println(response.Err)
}
fmt.Println(response.String())
read, _ := conn.Cmd("HGETALL", "card:93").Map()
for k, v := range read {
fmt.Printf("%s\t%s\n", k, v)
}
}
}</pre>
<p>Bu kez HMSET komutunu kullanarak bir hash üretiliyor. card:93 olarak belirtilmiş bir key söz konusu. Bu verinin nickName, greetings, price, attack, defense ve owner isimli alanları bulunuyor. Bir takım test verileri koyarak redis'e gönderiyoruz. Okuma kısmında ise HGETALL komutunun çağırılması söz konusu. Ancak dikkat çekici nokta bu seferki çağrım sonrası Map isimli metodun kullanılması. Bu sayede hash içerisindeki key ve value bilgilerini dolaşabileceğimiz map türünden bir nesneyi elde edebiliyoruz. Sonrasında range fonksiyonunu kullanarak ilgili key:value çiftlerini ekrana yazdırıyoruz. İşi eğlenceli hale getirmek için farklı bir şekilde renklendirdiğim komut satırının çalışma zamanı çıktısı aşağıdaki gibi.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/goredis_4.gif" alt="" /></p>
<h2>Go Tarafında Veriyi Yapı(Struct) Olarak Ele Alsak</h2>
<p>Lakin bir şeyler eksik gibi. Sakladığımız verinin kendisini belki de Go tarafından başka şekilde ifade edebiliriz. Örneğin oyun kartlarına ait bilgileri taşıyan bir yapı(struct) tasarlayıp onu bu senaryoda ele almamız daha doğru olabilir. O zaman kodları aşağıdaki hale getirerek yolumuza devam edelim.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package main
import (
"fmt"
"strconv"
"github.com/mediocregopher/radix.v2/redis"
)
func main() {
aragorn := Card{NickName: "Aragorn", Greetings: "Well Met!", Price: 9, Attack: 10, Defense: 12, Owner: "Luktar"}
AddCard(aragorn, "card:45")
card := GetCard("card:45")
card.ToString()
}
func AddCard(card Card, id string) {
conn, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println(err.Error())
} else {
defer conn.Close()
response := conn.Cmd("HMSET", id, "nickName", card.NickName, "greetings", card.Greetings, "price", card.Price, "attack", card.Attack, "defense", card.Defense, "owner", card.Owner)
if response.Err != nil {
fmt.Println(response.Err)
}
fmt.Println(response.String())
}
}
func GetCard(id string) *Card {
card := new(Card)
conn, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println(err.Error())
} else {
defer conn.Close()
response, _ := conn.Cmd("HGETALL", id).Map()
card.NickName = response["nickName"]
card.Greetings = response["greetings"]
card.Owner = response["owner"]
card.Attack, _ = strconv.Atoi(response["attack"])
card.Price, _ = strconv.Atoi(response["price"])
card.Defense, _ = strconv.Atoi(response["defense"])
}
return card
}
func (card *Card) ToString() {
fmt.Printf("Nickname:%s\n", card.NickName)
fmt.Printf("Greetings:%s\n", card.Greetings)
fmt.Printf("Owner:%s\n", card.Owner)
fmt.Printf("Price:%d\n", card.Price)
fmt.Printf("Attack:%d\n", card.Price)
fmt.Printf("Defense:%d\n", card.Defense)
}
type Card struct {
NickName string
Greetings string
Price int
Attack int
Defense int
Owner string
}</pre>
<p>İlk olarak Card isimli bir struct tasarladığımızı söyleyelim. İçerisinde Redis'teki Hash içeriğine karşılık gelen alanları barındırmakta. AddCard fonksiyonu parametre olarak gelen bir Card nesnesinin içeriğini kullanarak Redis üzerinde yeni bir Hash oluşturma işini üstleniyor. Fonksiyonun bir önceki örnekteki ekleme operasyonundan tek farkı değerleri almak için parametre olarak gelen Card örneğini kullanılması. card:45 benzeri key değeri için de id isimli bir parametre kullanmaktayız. GetCard metodu id bilgisine göre Redis üzerinden bir Card içeriğini çekmek üzere tasarlanmış durumda. Cmd üzerinden gidilen Map fonksiyonu ile redis tarafında tutulan içeriği almaktayız. Gelen içerikteki değerler string içerikte olacaktır. Bu nedenle strconv paketinden gerekli dönüştürme operasyonlarını kullanmamız gerekebilir. Card tipinin Price, Attack ve Defense gibi alanları int tipinden olduğu için Atoi tür dönüştürme metodundan yararlandık. Bulunan içeriğe göre değerleri atanan Card nesnesi olarak geriye döndürüyoruz. Card yapısına uygulanan ToString metodu ile de içeriği ekrana bastırıyoruz. İşte örnek çalışma zamanı çıktısı.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/goredis_5.gif" alt="" /></p>
<p>Pek tabii olmayan bir key değerini almaya çalışırsak içeriği boş bir yapı örneği elde ederiz. Söz gelimi card:100 sistemimizde bulunmuyor. Bu anahtar için program çıktısı aşağıdaki gibi olacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/goredis_6.gif" alt="" /></p>
<h1>Demek ki</h1>
<p>Teorimiz oldukça basit. Redis komutlarını çalıştırmak için paketin Cmd fonksiyonundan yararlanılabilir. Basit bir string kullanımından hash, list, set, sortedSet gibi veri yapılarına kadar gidilebilir. Dolayısıyla temel CRUD operasyonlarını basitçe ele alabiliriz. Bu noktada Redis'in komutlarını incelemekte ve detaylı bir şekilde öğrenmekte yarar olduğu kanısındayım. Go ile olan entegrasyonda işleri kolaylaştırmak ve programatik alanda veri türlerini tip bazında ifade etmek için yapılardan<em>(struct)</em> ve bu yapılara uygulanan yardımcı metodlardan yararlanılabilir. Yazımızda geçen örneği daha da geliştirmek elinizde. Söz gelimi Go'nun web programlama kabiliyetlerini baz alarak MVC<em>(Model View Controller)</em> yapısına uygun bir programı Redis ile çalışacak şekilde tasarlayabilirsiniz. Ya da arayüzle uğraşmak istemiyorsanız bir REST servis geliştirip temel veri operasyonlarının bu servis üzerinden gerçekleştirilmesini deneyebilirsiniz. Denemeye değer. Ben şimdilik dinlenmeye çekileceğim ama konuyu buradan alıp ileriye taşımak sizin elinizde. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2017-08-03T21:16:00+00:00golanggo programming languageredisnosqlbsenyurtBir Cumartesi gecesiydi. Dışarıda hava nemli ve sıcaktı. Bir süre önce başlayan yağmurun sesi çalışma odama tatlı tatlı geliyordu. Biraz da toprak kokusu. Evde el ayak çekilmiş sakin bir ortam oluşmuştu. GoLang öğrenme çalışmaları devam ediyordu. Dili ufak ufak kavramaya başlamıştım ama örnek senaryolar işleterek ilerlemem gerektiğini de biliyordum. Derken aklıma eskiden .Net tarafında kullandığım NoSQL sistemlerinden olan Redis ile ilgili bir şeyler yapmak geldi.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=01ad4dfc-98bf-4a06-889f-c416f354dd6d2https://www.buraksenyurt.com/trackback.axd?id=01ad4dfc-98bf-4a06-889f-c416f354dd6dhttps://www.buraksenyurt.com/post/golang-redis-ile-anlasmak#commenthttps://www.buraksenyurt.com/syndication.axd?post=01ad4dfc-98bf-4a06-889f-c416f354dd6dhttps://www.buraksenyurt.com/post/golang-unit-test-yazmakGoLang - Unit Test Yazmak2017-07-26T21:31:00+00:00bsenyurt<p><img style="float: right;" src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/gotesting_5.gif" alt="" />Merhaba Arkadaşlar,</p>
<p>Aranızda hala birim test(Unit Test) yazmayan/yazmamış olan var mı? diyerek konuya giriş yapmak istiyorum. Yazdığımız atomik fonksiyonelliklerin taşınan ortamlarda başımızı ağrıtmasını istemiyorsak birim testlerini mutlaka yazmalıyız. Üstelik iyi yazmalıyız. Belki birim testler uygulama geliştirme süresini uzatabilirler ancak uzun vadede kalp krizi geçirme riskini de azaltırlar. Üstelik test senaryoları sayesinde gerçekten ne yapmak istediğimizin farkında olarak da hareket edebiliriz. Eğer test güdümlü yaklaşımla<em>(Test Driven Development)</em> ilerliyorsak bilinçli olarak yaptırılan hata sonrası kodun çalışır hale getirilmesi ve iyileştirilmesi(Refactoring) de önemli kazanımlarımızdır<em>(Red-Green-Blue konusuna bir bakın)</em> En önemlisi de beklenen testleri başarılı bir şekilde aşmış temiz bir kodun ortaya çıkmasıdır.</p>
<p>Neredeyse her programlama dilinin Unit Test yazılmasına yönelik imkanları vardır. Geliştirme IDE'lerinde pek çok kolaylık bulunmaktadır. Çoğu ortam zaten standart kütüphaneler veya paketlerler ile bizleri olabildiğince Unit Test yazmaya yönlendirir. GO tarafında bu iş için dahili paketlerden olan testing kullanılmakta. Pek tabii github üzerinden bulabileceğiniz farklı test paketleri de mevcut. Bu kısa yazımızda basit bir Unit Test'in nasıl yazılabileceğini incelemeye çalışacağız.</p>
<h1>Önce Anlamsız İki Fonksiyon</h1>
<p>İşe ilk olarak anlamsız iki fonksiyon içeren aşağıdaki kod parçasını yazarak başlayabiliriz <em>(Amacımız GO tarafında Unit Test'lerin nasıl yazıldığını kurcalamak)</em> Operations.go içerisinde daire alanı hesaplayan ve n sayıda float32 tipinden sayının toplamını bulan birer fonksiyon(Variadic) bulunmaktadır.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package operations
import (
"math"
)
func CircleSpace(r float32) float32 {
return math.Pi * (r * r)
}
func Sum(numbers ...int) int {
var total int = 0
for _, n := range numbers {
total += n
}
return total
}</pre>
<p>Tasarladığımız paketteki CircleSpace ve Sum isimli operasyonlar için birer test metodu yazalım.</p>
<h1>Test Paketinin Yazılması</h1>
<p>GO'nun alışageldiğimiz kurallarına göre bir paket içerisinde yer alan fonksiyonların testini içeren ayrı bir dosyanın _test şeklinde isimlendirilerek oluşturulması gerektiğini söylesem sanıyorum yadırgamazsınız. Bu bana çok mantıklı geliyor. Paketlerin adlarına baktığımızda kimin test dosyası olduğunu görmemiz kolay. Anlamsal bir bütünlük oluşuyor ve herkes aynı stilde test dosya adı vermek durumunda. Güzel bir standart oluşturulduğu kesin. Örneğimize göre bu dosyanın adı operations_test.go şeklinde olmalı. Farklı bir isim verip test etmek istersek aşağıdaki gibi bir sonuçla karşılaşma ihtimalimiz oldukça yüksek.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/gotesting_3.gif" alt="" /></p>
<p>Gelelim operations_test.go içeriğine.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">package operations
import (
"testing"
)
func TestCircleSpace(t *testing.T) {
//var expected float32 = 314.159271
var expected float32 = 314.15
calculated := CircleSpace(10)
if expected != calculated {
t.Errorf("Test Fail : Calculated [%f]\tExpected [%f]\n", calculated, expected)
}
}
func TestSum(t *testing.T) {
//expected := 13
expected := -1
calculated := Sum(3, 4, 1, 5)
if expected != calculated {
t.Errorf("Test Fail : Calculated [%d]\tExptected [%d]\n", calculated, expected)
}
}</pre>
<p>Öncelikle test paketinin adının test edeceğimiz paket ile aynı olduğuna dikkat edelim. Diğer yandan testing paketini de import ediyoruz. TestCircleSpace ve TestSum isimli fonksiyonların testing.T tipinden olan değişken ile test ortamına log bildiriminde bulunmamız mümkün ki bunu Errorf fonksiyon çağrıları ile sağlamaktayız. Test akışı son derece pratik. Beklenen ve hesaplanan değerleri alıp karşılaştırıyoruz. Eğer aynı değillerse testin hatalı sonlandığını ifade edecek şekilde log çıktısı bırakıyoruz. Hepsi bu.</p>
<h1>Sonuçlar</h1>
<p>İlk olarak senaryomuzu beklenmeyen sonuçlar için test edelim. Bu durumda iki fonksiyon testinin de Fail olmasını bekliyoruz. LiteIDE üzerinden Test seçeneği ile veya komut satırından go test ile gerçekleştirilen işlemlerin sonucu aşağıdaki ekran görüntüsündeki gibi olacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/gotesting_1.gif" alt="" /></p>
<p>Komut satırından test yaparken o klasörde sadece go test yazmamız yeterlidir. Test dosyasının adını vermeye gerek yoktur. _Test uzantısı onu ele veriyor diyebiliriz. Buna göre bir klasörde n sayıda test dosyası varsa tamamını tek seferde çalıştırma imkanımız da olabilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/gotesting_4.gif" alt="" /></p>
<p>Görüldüğü gibi elde edilen sonuçlar istenen sonuçlar olmamış ve Fail bildirimleri alınmıştır. Şimdi beklediğimiz değerlerin yorum satırlarını kaldıralım. Bu durumda her iki testte başarılı olmalı. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2017/06/gotesting_2.gif" alt="" /></p>
<p>Dikkat edileceği üzere GO tarafında birim testler yazmak oldukça kolay. O zaman bundan sonraki ilk geliştirmenizde elinizdeki atomikleri önce TDD ilkelerine uyarak yazmaya gayret edin. Hatta FizzBuzz kod katasını baz alıp GO ile yazmayı deneyebilirsiniz. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2017-07-26T21:31:00+00:00golanggo programming languagetest caseunit testtestbirim testtest güdümlü programlamatest driven developmentgobsenyurtAranızda birim test(Unit Test) yazmayan hala var mı? diyerek konuya giriş yapmak istiyorum. Yazdığımız atomik fonksiyonelliklerin taşınan ortamlarda başımızı ağrıtmasını istemiyorsak birim testleri mutlaka yazmalıyız. Belki birim testler uygulama geliştirme süresini uzatabilirler ancak uzun vadede kalp krizi geçirme riskini de azaltırlar. Üstelik test senaryoları sayesinde gerçekten ne yapmak istediğimizin farkında olarak da hareket edebiliriz. Eğer test güdümlü yaklaşımla ilerliyorsak bilinçli olarak yaptırılan hata sonrası kodun çalışır hale getirilmesi ve iyileştirilmesi(Refactoring) de önemli kazanımlarımızdır.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=1542c63b-5307-48ae-9cf5-80ee03bfa8550https://www.buraksenyurt.com/trackback.axd?id=1542c63b-5307-48ae-9cf5-80ee03bfa855https://www.buraksenyurt.com/post/golang-unit-test-yazmak#commenthttps://www.buraksenyurt.com/syndication.axd?post=1542c63b-5307-48ae-9cf5-80ee03bfa855