Go ile Restful API oluşturma

 

 

Bir süredir boş vakitlerimi Go dilini öğrenerek ve kodları inceleyerek geçiriyordum. Bu süre içerisinde de Go dilinin syntax'ına alıştıktan sonra, bence en iyi öğrenme yöntemlerinden biri olan, "kendine bir konu bul ve onu yaparak öğren" stratejisini izledim. Bunun için de yeni bir dile başlarken yaygın bir konu olan "To Do" uygulamasını restful standartlarına uyarak bir API olarak yazmaya karar verdim. 

Öncelikle neden Go dilini seçtiğimi kısaca açıklamak isterim.

Neden Go dilini öğrenmeliyim?

 

  • Açık kaynak

Go dili Google mühendisleri tarafından geliştirilmiş olup, kaynak kodları tamamen erişilebilir bir şekilde Github üzerinde yer almaktadır.

  • Platform bağımsız

Go dilini Windows, Mac OS ve Linux dağıtımlarında yazıp çalıştırabilirsiniz.(sadece çalıştıracağınız işletim sistemini build alırken belirtmeniz gerekiyor)

  • Öğrenmesi kolay

Go dilinin syntax'ı ilk başta farklı ve zor gelebilir fakat, bir yerden sonra akıcı bir şekilde yazdığınızı farkediyordunuz. Ayrıca eğer bir dili iyi bir seviyede biliyorsanız, Go diline alışmanız hiç de zor olmayacaktır. Özellikle fonksiyon isimlendirmelerinde sanki bir cümle yazıyormuş hissine kapılıyorum.

  • Performans

Go'da yazdığınız uygulamalar doğrudan işlemcinin anlayacağı formata dönüşür(compiled dil yapısına sahiptir.). Bu sebeple hem projenin derlenmesi hızlıdır hem de yüksek trafik alan projelerde iyi bir mimariyle beraber milyonlarca istek sorunsuz yönetilebilir. Ayrıca goroutines ve channel diye de harika özellikleri de var fakat onu şu an ki örnekte kullanmayacağım. Merak edenler onu da ayrıca araştırabilir. 

  • Temiz kod

Ana dili C# olan bir yazılımcı olarak projenin bir yerinden sonra sahipsiz kalan değişkenler, neden eklendiği belli olmayan ama projede olan kütüphaneler, kullanılmayan değişkenler ve metotlar ile sürekli karşılaşmaktayız. Go dilinde ise projeye eklediğiniz bir kütüphaneyi kullanmıyorsanız veya bir değişkeni tanımlayıp kodunuz içerisinde kullanmadıysanız Go projenizi derlemiyor. Böyle olunca da genellikle atladığınız bir kısım olduğu için siliyorsunuz veya bunu ille de yapmak isteyenler için özel tanımladığı bir karakteri "_" kullanmanı istiyor. Ama günün sonunda tamamen işe yarayan parçalardan oluşan yalın bir kodunuz oluyor.

  • Cloud desteği

AWS, Azure, Google Cloud gibi popüler hizmet sağlayacılar üzerinde go dilinde yazılmış projelerinizi hızlıca canlıya çıkabilirsiniz. Ayrıca Go dilinde yazdığınız bir uygulamayı kolayca Docker container içerisine de yerleştirebilirsiniz.

  • Popüler

Fazla kafa karıştırmak istemediğim için detayına inmediğim bir çok iyi özellikle beraber hem ülkemizde, hem de diğer ülkelerde backend dili olarak Go kullanımı da her gün artmakta. Ülkemizde geçtiğimiz aylarda Go Türkiye tarafından büyük bir online etkinlik de yapıldı. Go ile ilgili benzer etkinlikleri kaçırmamak için takip edebilirsiniz.

 

Şimdi biraz kod yazalım!

Kısa bir bilgi olarak; Go'da projeleri ana dizin altında genellikle aşağıdaki dizine kurulur. 

  • $HOME/go Mac OS ve Linux sistemler
  • %USERPROFILE%\go Windows sistemler

Yeni projeleri de bu dizin altında \src\github.com\{kullanıcı.adınız} formatında oluşturmanız hem derleme aşamasında sistem değişkenleriyle sık sık oynamamanızı, hem de yeni bir kütüphaneyi projeyi hızlıca entegre etmenize imkan tanırsınız. Eğer Go'ya yeni başlıyorsanız şimdilik bu şekilde ilerlemeniz detaylarda kaybolmanızı engeller, konudan sapmamanızı sağlar.

Makalenin devamında Go nasıl kurulur, hangi editörlerde kullanılır konularını atlayacağım. Bunun için en altta yer alan kaynaklardan faydalanabilirsiniz. Ayrıca Go syntax'ı hakkında da açıklamalara bu makalede değinmeyeceğim. Bunun için Go'nun kendi sitesindeki kısa bir tur olarak planlanmış eğitime buradan göz atabilirsiniz.

Ben bu makaleyi yazarken Go'nun güncel sürümü olan "1.15.6" versiyonunu ve IDE olarak da "Visual Studio Code" kullanıyorum. Projeyi hem Windows 10'da hem de Mac OS Catalina'da çalıştırdım. 

Go ile API Oluşturmak

Go uygulamamızı bir web uygulamasına çevirmek için öncelikle bunun için geliştirilmiş kütüphanelerden birini projeye eklememiz gerekiyor. Bunun için ben "Echo" 'yu seçtim. Hem performans olarak iyi sonuç veriyor hem de dökümantasyon anlamında başlangıç için faydalı olacağını düşündüm.

*echo'nun diğer popüler framework ile karşılaştırması.(düşük değer iyi sonuç)

 

Ayrıca eklenecek to-do kayıtlarının tutulması için de açık kaynak kodlu bir proje olan SQLite veritabanını kullanacağım. Veritabanını uygulamanın kendi içerisinde oluşturup tutacağız. Herhangi bir uzak sunucuya bağlantı yapmayacağız.

 

 

Projemize aşağıdaki dosyaları sırasıyla ekleyerek başlayalım.

İlk dosyamız bir Go projesi için olmazsa olmazımız, "main.go" dosyası. Bir go uygulaması ilk başta main içerisinden ayağa kalkar. Burada da main() metodu olmazsa olmazdır. Ayrıca uygulama ayağa kalkarken önce init() metodu içerisindeki tanımları uygular ardından main içerisini işlemeye başlar. Biz de uygulamamızın hem web framework, hem de middleware tanımlarını init() içerisinde yapıyoruz.   

Bir restful api'da endpointlerimiz nasıl olmalı ve hangi http metotlar üzerinden ulaşılmalı konularına buradaki makalemde değinmiştim. Merak edenler restful ile ilgili kısa bilgi almak için buradan okuyabilir.

Burada önemli noktalarımızdan biri "e := echo.New()" ile echo web framework'üne ait bir referans oluşturduktan sonra API'da kullanacağımız tüm endpointleri bu referans içerisine tanımlıyoruz. Böylelikle gelen HTTP isteğini karşılayacak fonksiyona doğrudan gelen bilgileri aktarıyoruz.  Servis kodlarına geldiğimizde de her bir endpointe karşılık gelen fonksiyonda echo'ya ait HTTP Context bilgisini parametre olarak ekleyeceğiz. 

main.go

package main

import (
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
	_ "github.com/mattn/go-sqlite3"
	"github.com/yigitnuhuz/gotodo/services"
)

func init() {
	// Echo framework için yeni bir instance oluşturulur
	e := echo.New()

	// Middleware tanımlamaları yapılır
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Endpoint'lerimiz oluşturulur.
	e.GET("/", services.Hello)

	e.GET("/todos", services.AllTodos)
	e.POST("/todos", services.CreateTodo)

	e.GET("/todos/:id", services.GetTodo)
	e.PUT("/todos/:id/complete", services.UpdeteTodoIsComplete)
	e.PUT("/todos/:id/uncomplete", services.UpdeteTodoIsUncomplete)
	e.DELETE("/todos/:id", services.DeleteTodo)

	// 3200 portundan API'ı ayağa kaldıralım
	e.Logger.Fatal(e.Start(":3200"))
}

func main() {

}

 

API çalışırken kullanılacak veritabanına ait tanımı aşağıdaki gibi bir config dosyası içerisinde yapalım. Ayrıca API için gerekecek veritabanı işlemleri için de aktif bağlantıyı oluşturup buradan dönüyoruz. 

"db.Prepare()" içerisinde mevcutta veritabanımız var mı onu kontrol ediyoruz. Böylelikle ilk çalıştırmada eğer veritabanımız yoksa oluşturulmasını sağlıyoruz.

config.go

package config

import "database/sql"

func GetDb() (db *sql.DB, err error) {
	db, err = sql.Open("sqlite3", "./gotodo.db")

	statement, _ := db.Prepare("CREATE TABLE IF NOT EXISTS Todos (Id INTEGER PRIMARY KEY, Detail TEXT, Completed BIT);")
	statement.Exec()
	return
}

 

Son olarak da endpointlerde tanımladığımız işlemleri temel servislerimizi aşağıdaki gibi oluşturuyoruz. Servis içerisinde bizim aslında yapmasını istediğimiz işlemleri ayrı birer fonksiyon olacak şekilde oluşturuyoruz. 

services.go

package services

import (
	"database/sql"
	"net/http"
	"strconv"

	"github.com/labstack/echo"
	"github.com/yigitnuhuz/gotodo/config"
)

type Todo struct {
	Id        int    `json:"Id"`
	Detail    string `json:"Detail"`
	Completed bool   `json:"Completed"`
}

func Hello(c echo.Context) error {
	return c.String(http.StatusOK, "API'dan selamlar...")
}

func AllTodos(c echo.Context) error {
	db, _ := config.GetDb()
	defer db.Close()

	rows, _ := db.Query("SELECT Id, Detail, Completed FROM Todos")
	defer rows.Close()

	todos := []Todo{}

	for rows.Next() {
		todoItem := Todo{}
		rows.Scan(&todoItem.Id, &todoItem.Detail, &todoItem.Completed)
		todos = append(todos, todoItem)
	}

	return c.JSON(http.StatusOK, todos)
}

func CreateTodo(c echo.Context) error {
	db, _ := config.GetDb()
	defer db.Close()

	u := &Todo{}

	if err := c.Bind(u); err != nil {
		return err
	}

	statement, _ := db.Prepare("INSERT INTO Todos (Detail, Completed) VALUES (?, ?)")
	statement.Exec(u.Detail, u.Completed)
	defer statement.Close()

	return c.JSON(http.StatusCreated, u)
}

func GetTodo(c echo.Context) error {
	db, _ := config.GetDb()
	defer db.Close()

	id, _ := strconv.Atoi(c.Param("id"))

	var todo Todo

	statement, _ := db.Prepare("SELECT Id, Detail, Completed FROM Todos WHERE Id = ?")
	err := statement.QueryRow(id).Scan(&todo.Id, &todo.Detail, &todo.Completed)
	defer statement.Close()

	if err == sql.ErrNoRows {
		return c.NoContent(http.StatusNotFound)
	} else if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}

	return c.JSON(http.StatusOK, todo)
}

func UpdeteTodoIsComplete(c echo.Context) error {
	db, _ := config.GetDb()
	defer db.Close()

	id, err := strconv.Atoi(c.Param("id"))

	if err != nil {
		panic(err.Error())
	}

	statement, _ := db.Prepare("UPDATE Todos SET Completed = 1 Where Id = ?")
	_, err = statement.Exec(id)
	defer statement.Close()

	if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}

	return c.NoContent(http.StatusOK)
}

func UpdeteTodoIsUncomplete(c echo.Context) error {
	db, _ := config.GetDb()
	defer db.Close()

	id, err := strconv.Atoi(c.Param("id"))

	if err != nil {
		panic(err.Error())
	}

	statement, _ := db.Prepare("UPDATE Todos SET Completed = 0 Where Id = ?")
	_, err = statement.Exec(id)
	defer statement.Close()

	if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}

	return c.NoContent(http.StatusOK)
}

func DeleteTodo(c echo.Context) error {
	db, _ := config.GetDb()
	defer db.Close()

	id, err := strconv.Atoi(c.Param("id"))

	if err != nil {
		panic(err.Error())
	}

	statement, _ := db.Prepare("DELETE FROM Todos Where Id = ?")
	statement.Exec(id)
	defer statement.Close()

	if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}

	return c.NoContent(http.StatusOK)
}

 

Burada öncelikle bir yapılacaklar kaydına ait modele ihtiyacımız var. Bunun için aşağıdaki gibi bir struct tanımladım. Nesne tabanlı programlamaya alışık olanlar model'e karşılık geldiğini düşünebilirler.

type Todo struct {
	Id        int    `json:"Id"`
	Detail    string `json:"Detail"`
	Completed bool   `json:"Completed"`
}

API isteklerinde yapacağız isteklerde ve cevaplarda bu modeli kullanacağım. Ayrıca Go'da eğer bir struct'ı json'dan eşleştireceksek, içerisine json içerisinde gelecek parametre adını da yazıyoruz. Bu duruma bir örnek verecek olursam eğer aşağıdaki gibi bir tanımlamada;

IsCompleted bool   `json:"Completed"`


Request body içerisinde gelen "Completed" alannı, "IsCompleted" parametremize eşleştirebiliriz. 

Basit bir endpointi aşağıdaki gibi oluşturabiliriz. Ben burada API ayağa kalktığında "/" ana dizininde çalışacak bir mesaj dönen fonksiyon oluşturdum. Bunu incelediğimizde;

func Hello(c echo.Context) error {
	return c.String(http.StatusOK, "API'dan selamlar...")
}

"Hello" fonksiyonumuz main içerisinde tanımladığımı echo context'ini içerisine alıyor. İçerisinde ise sadece metin olarak selamımızı iletiyor. Burada "return" yaparken c.String, c.JSON, c.XML gibi bir çok türde talebimize göre cevap dönebiliriz. Ayrıca dönüş yaparken hangi http kodunu(200,400,401,500) döneceğimizi de belirtebiliyoruz. Bunu belirtirken de isterseniz hazır http mesaj şablonlarından ismiyle beraber, isterseniz de doğrudan http koduyla dönüş yapabiliyorsunuz. Bunun için Kayıt/sayfa bulunamadı - 404 dönmek için aşağıdaki iki kullanım da doğrudur. Fakat kod okunurluğu açısından bana ismiyle belirtmemiz daha doğru geldi.

return c.NoContent(404)
return c.NoContent(http.StatusNotFound)

 

API üzerinden tanımladığımız endpointlerin üzerinden geçecek olursam;

GET "/": API çalıştığına dair mesaj döner

GET "/todos": Tüm todo listesini döner

POST "/todos": Yeni bir todo oluşturmamızı sağlar

GET "/todos/:id": Verilen Id değerine ait todo kaydını döner

PUT "/todos/:id/complete": Verilen Id değerine ait todo kaydını tamamlandı olarak işaretler

PUT "/todos/:id/uncomplete": Verilen Id değerine ait todo kaydını tamamlanmadı olarak işaretler

DELETE  "/todos/:id": Verilen Id değerine ait todo kaydını siler

 

Bu üç dosyayı oluşturduktan sonra projenin aşağıdaki yapıda olmasını bekliyoruz.

 

 

Go'da sevdiğim ve üzerinde durmak istediğim noktalardan biri de "defer" komutu oldu. Bu komutu kullandığımızda peşinden gelen kod bloğu fonksiyonun neresinde olursa olsun, foksiyon çalışmasını bitirdikten sonra çalışır. Bir örnekle açıklayacak olursam, bazı yüksek kullanım alan projelerde açık kalan veritabanı bağlantıları zamanla şişerek hem trafiği yavaşlatmakta hem de bir yerden sonra yeni kullanıcıların bağlanmasına engel olmakta. Bunun için veritabanına istediğimizi verdikten sonra ve istediğimizi aldıktan sonra bağlantımızı kapatmalıyız. Bunun için ben, veritabanına ait bağlantıyı aldıktan sonra peşinden fonksiyon bittiğinde çalışması için bağlantıyı kapatmayı söylüyorum.

db, _ := config.GetDb()
defer db.Close()

Bunu C#'da kullanılan finally bloğunun farklı bir kullanımı gibi düşünebilirsiniz.  "defer" örnekleri amacınıza göre çeşitlendirilebilirsiniz.

 

Go projesinin çalıştırılması

 Projemizin bulunduğu ana dizinde terminalde gelip aşağıdaki komutu çalıştırdığımızda projemiz ayağa kalkacaktır.

go run .

 

 

Şimdi diğer endpointleri de kullanarak bir yapılacaklar listesi oluşturalım. Bunun için Postman'i kullanacağım.

İlk olarak "localhost:3200/todos" adresine bir POST isteği atarak ilk maddemizi ekliyoruz.

Ardından tüm todo listesini dönen "localhost:3200/todos" adresine bir GET isteği yapıyoruz. Aşağıdaki gibi oluşturduğumuz madde bize döndü.

Maddemizi tamamlandı olarak işaretlemek için "localhost:3200/todos/1/complete" adresine bir PUT isteğinde bulunuyoruz. BUrada linkte yer alan 1 bizim maddemizin ID değeri. 

Maddemizi tamamlandı olarak işaretledikten sonra todo listesine tekrardan istek yaptığımızda aşağıdaki gibi maddemizin tamamlandı olarak işaretlendiğini görebiliriz.

Şimdi yeni bir madde daha ekleyelim ve listeyi tekrar çağıralım.

Gördüğünüz gibi listemizde iki madde oldu fakat ben son eklediğim 2 ID'li maddeyi silmek istiyorum. Bunun için aşağıdaki adresine bir DELETE isteği yapacağım.

Silindiğinden emin olmak için listeyi tekrar çağırdığımda 2. maddenin silinmiş olduğunu görüyorum.

 

Uygulamamız çalışırken echo üzerinde gelen istekleri loglamak için init() içerisinde loglamayı kullanmıştım.

e.Use(middleware.Logger())
Bu yüzden gelen istekleri terminal ekranında da teker teker görebilmekteyim.
 
 

Bu makalemde temel olarak Go dilinde bir restful api oluşturup çalıştırdım. Bir sonraki seride api'ımıza yeni özellikler ekleyip daha da geliştireceğiz. 

Projenin son halini Github üzerinden takip edebilirsiniz. Proje üzerine eklemeler yapmaya devam ettiğim için güncel hali makaleden farklı olabilir.

 

Faydalandığım Kaynaklar

 

https://github.com/yigitnuhuz/gotodo

https://tour.golang.org/welcome/1

http://blog.oguzhan.info/?p=870

https://medium.com/bili%C5%9Fim-hareketi/go-programlama-diline-genel-bak%C4%B1%C5%9F-fb802539bfc

https://medium.com/trendyol-tech/concurrency-and-channels-in-go-bbc4dea75286

Add comment