- Go, backend geliştirmenin aşırı karmaşıklığını azaltan bir seçenek olup; hızlı derleme, tek binary dağıtımı ve güvenilir bağımlılık yönetimi temel avantajlarıdır
- Go; decorator, metaclass, macro, trait, monad gibi karmaşık soyutlamalar yerine struct, fonksiyon, interface, goroutine ve channel merkezli sade bir dil tasarımını seçer
embed, html/template, net/http, database/sql, encoding/json, go test, pprof gibi standart kütüphane ve temel araçlarla web uygulaması, veritabanı, test, benchmark ve profiling işlemleri yapılabilir
- goroutine, yaklaşık 2KB maliyete sahip stackful bir yürütme birimidir; channel,
sync.Mutex, race detector ve context.Context sayesinde eşzamanlılık ve iptal yayılımı basitçe ele alınabilir
go mod init, go build, scp, systemctl restart akışı; node_modules, karmaşık Docker·Kubernetes kurulumları ve aşırı mikroservisler yerine tek bir Go binary’si ve Postgres merkezli sade dağıtımı önerir
Neden Go’yu seçmelisiniz?
- Go, backend geliştirmenin aşırı karmaşıklığını azaltan bir seçenek olup; hızlı derleme, tek binary dağıtımı ve güvenilir bağımlılık yönetimi temel avantajlarıdır
- Frontend tarafında HTML nasıl aşırı karmaşıklığa karşı bir alternatif olarak kaldıysa, Go da 10 yıldan uzun süredir backend sadeleştirmesi için bir seçenek olarak varlığını sürdürüyor
- Basit bir form sunmak ya da saniyede yaklaşık 40 istek düzeyindeki bir CRUD uygulaması için bir sürü Node paketi, TypeScript build araçları, Kubernetes, Rails platform ekibi ve hatta Rust ile yeniden yazım kullanmak fazladır
- Go’nun odağı “zekice soyutlamalar”dan çok okunması kolay kod, dağıtılabilir çıktı ve düşük operasyonel yüktür
Kasıtlı olarak sıkıcı tasarlanmış bir dil
- Go’nun sıkıcı hissettirmesinin nedeni kasıtlı tasarımıdır; decorator, metaclass, macro, trait, monad gibi karmaşık soyutlamalar sunmaz
- Temel yapı taşları kabaca struct, fonksiyon, interface, goroutine ve channel ile sınırlıdır
- Amaç, dil özelliklerini kısa sürede okuyup aynı gün üretken biçimde kod yazabilecek kadar sade kalmaktır
- Sıkıcılık, ekip kod tabanında bir avantaja dönüşür
- Geçen ay işe giren bir junior, 2 yıl önce principal tarafından yazılmış kodu okuyabilir
gofmt tek bir formatı zorunlu kıldığı için kod stili tartışmaları azalır
- Dilin kendisi, aşırı karmaşık soyutlamaların kod tabanına sokulmasını zorlaştırır
Standart kütüphane framework gibi davranır
- Go’da ayrı bir web framework’ü olmadan da yalnızca standart kütüphane ile web uygulaması geliştirilebilir
embed, html/template, net/http kullanarak HTML template’lerini binary’ye gömüp HTTP handler’larıyla render eden bir uygulama kurulabilir
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "asshole",
})
})
http.ListenAndServe(":8080", nil)
}
- Bu örnek çalışan bir web uygulamasıdır ve HTML template’leri derlenip binary’ye dahil edilir
webpack, Vite, development server ya da devasa node_modules olmadan go build sonrası tek bir dosya dağıtılabilir
- Standart kütüphane ve temel araçlarla başlıca backend işleri yapılabilir
- Veritabanı:
database/sql
- JSON:
encoding/json
- Diğer servis çağrıları:
net/http client
- Eşzamanlı çalışma:
go anahtar sözcüğü
- Test:
go test
- Benchmark:
go test -bench
- Profiling:
pprof
Derinlikli bir standart kütüphane yapısı
-
io.Reader ve io.Writer
io.Reader ve io.Writer, her biri yalnızca tek metoda sahip interface’lerdir ama Go ekosisteminin genelinde önemli bir temel oluştururlar
- HTTP response body’sini bir gzip writer’a bağlayıp oradan disk dosyasına aktarma gibi bileşimler az kodla kurulabilir
- Temel paketler bu iki interface’i paylaştığı için aynı kalıplar birçok yerde tekrar tekrar kullanılabilir
-
context.Context
context.Context, iptal yayılımı için standart yöntemdir
- Kullanıcı tarayıcı sekmesini kapattığında request context iptal olur ve ardından veritabanı sorgusu ile alt HTTP çağrıları da iptal edilebilir
- goroutine sızıntılarından veya connection pool tüketen zombi sorgulardan kaçınmak için context’i ilk argüman olarak geçirip buna saygı göstermek gerekir
-
Encoding paketleri
encoding/json, encoding/xml, encoding/csv, encoding/binary paketlerinin tamamı standart kütüphanede yer alır
- struct tag kalıbı ve pointer ile decode etme deneyimi benzer olduğu için birini öğrenince diğerlerini kullanmak da kolaylaşır
Acıyı azaltan eşzamanlılık modeli
- goroutine, doğrudan bir OS thread değildir; runtime’ın OS thread’leri üzerinde çokladığı stackful bir yürütme birimidir
- goroutine başlatma maliyeti yaklaşık 2KB’dır ve bir laptop’ta bile 100 bin tane oluşturulabilir
- channel, goroutine’ler arasında tür güvenli bir boru gibi çalışır; bir taraf gönderip diğer taraf aldığında senkronizasyonu runtime halleder
- Paylaşılan durum gerektiğinde
sync.Mutex kullanılabilir ve race detector veri yarışlarını bulur
- Paralel bir HTTP fetcher da ayrı kütüphane, framework veya
async/await ritüelleri olmadan yazılabilir
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
Gerçek bir CRUD route örneği
- Postgres’ten gönderileri okuyup HTML render eden CRUD tarzı bir route da tek ekrana sığacak kadar sade kurulabilir
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
- Bu örnek veritabanını, template’i ve HTTP handler’ı tek yerde gösterir
r.Context() SQL sorgusuna aktarıldığı için bağlantı kapanırsa sorgu da iptal edilebilir
- ORM, DI container, service layer ya da soyut taban sınıflarla dolu
controllers/ dizini olmadan yukarıdan aşağı okuyarak davranış anlaşılabilir
Hafta sonunu mahvetmeyen bağımlılık yönetimi
go mod init ile modül başlatıldığında bağımlılıklar go.mod ve go.sum içine kaydedilir
go.sum, gerçekten indirilen öğelerin kriptografik kaydıdır; böylece beklenenden farklı bir bağımlılığın gelmesi tespit edilebilir
node_modules dizini, geliştirme ortamı ile CI arasında lockfile drift, peer dependencies, optional dependencies, devDependencies, peerDependenciesMeta gibi karmaşıklıklar yoktur
- Offline build gerekirse
go mod vendor, bağımlılıkları vendor/ dizinine indirir ve toolchain bunu otomatik kullanır
- Tüm proje ve bağımlılıklar tek bir tarball içine konabilir; bu da operasyon ve güvenlik incelemesi açısından avantaj sağlar
Derleyiciyle birlikte gelen araçlar
- Go’nun temel araçları, third-party plugin’ler veya ayrı yapılandırma dosyaları olmadan gelir
gofmt, kod formatını standartlaştırır ve format tartışmalarını, ayrıca boşluk değişimlerinden doğan diff artışını azaltır
go vet, bariz hataları yakalamak için kullanılır
go test, testleri çalıştırır
go test -race, testleri race detector ile birlikte çalıştırıp veri yarışlarını bulur
go test -bench, benchmark’ları çalıştırır
go test -cover, test coverage durumunu gösterir
go tool pprof, çalışan production servisinin HTTP endpoint’i üzerinden CPU ve bellek kullanımının flame graph’ını almayı sağlar
Dağıtım bir kopyalama komutuyla biter
- Go dağıtımının temel akışı binary’yi derlemek, sunucuya kopyalamak ve çalıştırmaktır
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
- Bu akış; Dockerfile, multi-stage build, base image CVE uyarıları, Kubernetes manifest’leri, Helm chart, ArgoCD, service mesh ve sidecar olmadan dağıtıma izin verir
- Yaklaşık 12MB boyutunda statik linklenmiş bir binary ve 20 satırlık bir systemd unit dosyası ile production dağıtımı yapılabilir
- Docker gerçekten gerekiyorsa Go binary’sini
FROM scratch imajına koymak yeterlidir
Framework’lerle karşılaştırma
- Rails, Django, Express, Next.js gibi framework’lerin her birinin kendi dağıtım süreci, ORM’i, admin paneli, middleware’i, npm uyarıları ve değişen routing konvansiyonları gibi yükleri vardır
- Go binary’si derlenir ve çalıştırılır; 5 yıl sonra bile çalışabilecek istikrarı bir avantaj olarak sunar
- Framework’lerin daha hızlı terk edilmesi ya da bakımcılarının tükenmişlik yaşaması ihtimaline kıyasla, Go’nun sade çalışma modeli öne çıkar
Mikroservis yerine tek bir Go binary’si
- Mikroservisler varsayılan tercih olmamalıdır; önce bir monolith yazmak daha iyidir
- Önerilen yapı; tek bir Go binary’si, tek bir Postgres ve yalnızca gerçekten gerektiğinde tek bir Redis’tir
- HTML ile JSON API aynı porttan sunulabilir ve her şey tek bir VPS üzerinde çalışabilir
- Go, düşük goroutine maliyeti ve güçlü eşzamanlılık yapısı sayesinde saniyede 10 bin isteğe kadar zorlanmadan ölçeklenebilir
- Gerçekten ayırma ihtiyacı doğarsa, Go monolith içindeki paketler ayrı repository’lere taşınarak bölünebilir
- Interface’ler zaten mevcut olduğu için dil, ayrıştırmayı doğal olarak düşünen bir yapı kurmaya yardımcı olur
Generics ve hata işleme
if err != nil bir bug değil, bir özelliktir
- Her hata noktasında ne yapılacağına doğrudan karar vermeyi sağlar ve hataları gizlemez
try/catch iç içeliği hataları ortadan kaldırmaz; sadece onları production arızası yaşanana kadar saklayabilir
- Generics, Go 1.18 ile geldi ve gerektiğinde kullanılabilir
Sonuç
- Framework’ler, mikroservisler, Rust ile yeniden yazım ya da yeni bir JavaScript meta-framework her zaman gerekli değildir
go mod init çalıştırıp main.go yazmak, template’leri embed etmek, sonra derleyip dağıtmak gibi sade bir akış önerilir
- Sıkıcı seçim doğru seçimdir ve Go da o seçimdir
1 yorum
Lobste.rs görüşleri
Elçiyi suçlamak istemem ama bu tür blog üslubu yorucu ve çocukça. İlk başta komik gelmiş olabilir ama tekrarlandıkça rahatsızlık katlanarak artıyor
Yine de Go iyi. Yakın zamanda bir TypeScript projesinden bir Go projesine geçtim; ruh sağlığım ve iş motivasyonum hızla iyileşiyor
if err != nilifadesinin bir bug değil, özellik olduğunu kabul ediyorum ama yine de Go’nun en büyük kusuru olduğunu düşünüyorum. Toplam tipler (sum types) olsaydı, çalışma zamanı tip doğrulamalarına bel bağlamadan bunu çok daha ergonomik hale getirmek mümkün olurduBöyle yazacaksan en azından hakaretlerin biraz zekice olsun
Diğer yorumlara bakınca popüler olmayan bir düşünce gibi görünüyor ve kaba gelmek istemem ama Go’dan gerçekten nefret ediyorum
Go, eşzamanlılık için verimli bir runtime’ın üstüne şöyle böyle fena olmayan bir sözdizimi koyup Google’ın gücüyle ekosistemi ittiren bir dil. Onun dışında bana göre korkunç
En büyük sorun, onlarca yıllık programlama dili tasarımı araştırmasını hatta pratikteki iyi yöntemleri bile bilerek yok saymak için tasarlanmış gibi görünmesi. On yıllar sonra generics geldi gerçi
Her zaman dependent types kullanmak gerektiğini söylemiyorum ama bunun da bir ölçüsü var. Go’da modern bir dilde olması gereken veri modelleme, değişmez koşulları modelleme ve kodu yapılandırma özelliklerinin neredeyse hiçbiri yok. Rust’ın öğrenme eğrisi daha dik ama bu açılardan çok daha iyi; üstelik Rust kadar sofistike bir tip sistemi olmasa da gayet yeterli olunabilir. Derleme süresi dertse, basit ama işe yarayan özelliklerle hem hızlı hem de ifade gücü yüksek, sağlam bir tip sistemi yapılabilir
Ayrıca
if err != nil, kodu hata işleme gürültüsüyle kaplamanın olabilecek en kötü yolu bence. Go tarafının toplam tiplere neden bu kadar alerjisi olduğunu bilmiyorum. Bu konuda Java exception’ları bile daha iyi. Gerçekte olan şu: dilde hataları daha iyi ele alacak özellikler olmadığı için insanlar mümkün olan en kötü yamanın bir özellik olduğunu sanıyorZaten orijinal yazı ukalalık yapmasaydı ben de böyle bir yorum yazmazdım. “Sadece X kullan” aptalca bir laf. Kullanım durumuna uyan, rahat ve üretken olduğun aracı kullanırsın. Bu Go ise Go kullanırsın, değilse başka bir şey seçersin
Özellikle Google gibi, binlerce geliştiricinin olduğu ve insanların belirli bir ekipte ya da şirkette kısa süre kalabildiği organizasyonlarda bu işe yarıyor
Bu bağlamda, özellikle daha toy geliştiriciler için gelişmiş tip sisteminin yokluğu bir noktaya kadar avantaj bile olabiliyor. Çünkü temel tipler ya da struct’lar gibi en basit kavramların ötesinde tipler üzerine neredeyse hiç düşünmek gerekmiyor. Veri modellemek için neredeyse hiç araç vermiyor ama öte yandan fazla düşünmeden çok sayıda kod yazabiliyorsun
Dil seviyesinde doğruluk açısından pek iyi değil bence. Ama büyük organizasyonlarda insanlar monorepo analizi, CI/CD, canary testleri, gözlemlenebilirlik araçları gibi çevresel altyapılara daha fazla güveniyor. Bu altyapı, küçük organizasyonlara göre çok daha fazla yük taşıyor
Ben de benzer biçimde düşük bilişsel yük yüzünden Go’yu bir ölçüde seviyorum. Belirli projelere sadece arada bir kod yazıyorum ve şu an her gün uzun soluklu projelere derinlemesine dahil olmuyorum. Bir aydır bakmadığım bir kod tabanına girip bir saatten kısa sürede iş yapabilmek büyük avantaj. Ama karmaşık bir projede tam zamanlı geliştirici olsaydım muhtemelen daha az severdim
Go geliştiricileri temelleri doğru kurmaya odaklandı diye düşünüyorum. Çünkü bugüne kadarki dillerle programlama dili teorisi araştırma topluluğu temelleri ihmal etti. İnsanlar en kapsamlı tip sistemine saplanıp kalıyor ama tip sistemi ne kadar karmaşık ve ifade gücü yüksek olursa getirisi de o kadar azalıyor; ayrıca tip sistemine ne kadar emek verilirse verilsin korkunç paket yönetimi, ekibin yeni DSL’ler öğrenmesini gerektiren build araçları, tip bilgisine ya da üçüncü taraf paket dokümantasyon bağlantılarına otomatik link üretmeyen dokümantasyon sistemleri, zayıf standart kütüphane, ciddi performans sorunları, statik derleme stratejisinin yokluğu, acı veren build süreleri, dik öğrenme eğrisi, cezalandırıcı tip sistemi, okunması zor sözdizimi ve berbat editör entegrasyonu telafi edilemez
Go’da hiç veri modelleme özelliği yok demek açıkça yanlış. Her dilde veri ve değişmez koşullar modellenebilir; Go da bu modeli zorlayacak epey tip sistemi sunuyor
Rust harika ve yineleme hızının önemli olmadığı, bare metal’e dağıtım yapılan ya da doğruluk ve performans gereksinimlerinin çok güçlü olduğu durumlarda iyi bir seçim. Ama genel amaçlı uygulama geliştirme için, özellikle de ekip geliştirmesinde varsayılan tercih olarak iyi değil.
if err != nilçok yazılıyor olabilir ama saniye başına tuş vuruş sayısı yüzünden darboğaza giren kimse yoktur sanırımif err != nilifadesinin bir bug değil özellik olduğu ve sorun çıkabilecek her noktayı görünür kıldığı iddiası yanlışGerçekte zorunlu değil. Kendin kontrol etmezsen hatayı yok saymak daha kolay
Hataları ele alma ya da yayma biçimi konusunda Rust hâlâ parlayan örnek
Neyse ki son birkaç yılda üzerinde çalıştığım tüm Go projeleri, Go’nun zayıf yerleşik statik kontrollerinin üstüne golangci-lint koyuyordu. Açıkçası her Go projesinde zorunlu olmalı
Bu yazım modasından gerçekten nefret ediyorum ama yazının vermek istediği mesaja katılıyorum
“Volkswagen büyüklüğünde
node_modulesyok” demesi doğru ama bu, proje yerelindekinode_modulesyerine~/goaltındaki genel paket önbelleği demek sadecewc -l go.sumçalıştırası geliyorSayfayı açar açmaz “Hey, dipshit.” gördüm ve hemen kapattım
Programlama dillerini öven yazıların çoğundaki aynı sorunu taşıyor. Mevcut dilin ne kadar harika olduğundan çok, daha önce kullandığı dilin ne kadar korkunç olduğuna odaklanıyor
Yazar belli ki Ruby ve TypeScript, belki de Python yüzünden ciddi acı çekmiş ve Go bunu çözmüş. Ama ben Ruby ya da TypeScript kullanmadığım için yazı bana pek bir şey ifade etmedi
Yıllardır bunun düzinelerce varyasyonunu okumuşum gibi hissediyorum. Python ve JavaScript’ten farklı olarak statik tipleri var diye Haskell kullan. Perl ve Erlang’dan farklı olarak tek binary olarak dağıtılabiliyor diye Rust kullan. Ruby ve Tcl’den farklı olarak düzgün eşzamanlılık ve channel’ları var diye Elixir kullan
Yazarın kendine uyan dili bulmasına sevindim ama tavsiyesini izlemeyeceğim
Go’nun sıfır değeri bana hep kusur gibi gelmiştir. Kullanıcıyı varsayılanları açıkça belirtmeye zorlamak daha iyi olurdu bence. Onun dışında OCaml olmamasını hesaba katarsak oldukça iyi bir dil
boolalanınıntrueolması gereken bir JSON nesnesini marshal etmek çok zorDağıtım ve derleme deneyimi harika ama dilin kendisinde kod yazmaktan gerçekten nefret ediyorum. Her kullandığımda kötü bir deneyim oluyor. Go kadar kısıtlayıcı olmayan ama dağıtım deneyimi iyi olan başka diller var mı?
Go’da bir şeyi mi kaçırıyorum?
Yakın zamanda küçük bir Rails uygulaması dağıttım; o kadar çok ayar gerekiyordu ki Go’nun avantajını ister istemez daha çok takdir ettim
x86_64-unknown-linux-musliçin derlemeye başladım. Böylece tüm 64 bit Linux makinelerde doğrudan çalışan statik binary elde ediyorum. Sonrascpile kopyalayıp çalıştırıyorumHâlâ port atamak ve elle başlatmak gibi işler var ama biraz systemd büyüsüyle çözeceğim
Bundler ile tek bir çalıştırılabilir dosya üretilebiliyor; bunu başka dağıtımlardaki Linux makinelere atsan bile, hatta Qt kurulu olmasa bile, kullanıcı sadece yürütülebilir dosyayı çalıştırarak tüm GUI’yi kullanabiliyor
Ama OpenGL sürücüleri konusunda bazı pürüzler olduğu notunu düşmek gerekir. Hâlâ mümkün ama “kopyala ve çalıştır” kadar basit değil
Go’nun eşzamanlılık için tasarlandığını iddia ederken, yanlışlıkla kolayca paylaşılabilen ham pointer’ları yerleşik olarak sunması en büyük sorun bence
Sıkıcı olması tek başına sorun değil ama Go, gerçekten sıkıcı bir dil olmayı tuhaf biçimde başaramıyor bence
“Decorator yok” deniyor ama struct tags ve reflection var. Bunların nasıl etkileştiğini çalıştırmadan anlamak zor
Yapısal interface’ler ve reflection, davranışın uzaktan değişmesine yol açan korkutucu kaynaklar. Sadece bir struct’a yanlış bir metot ekledin diye bir kütüphanenin davranışı tamamen değişebilir
Dokümantasyon açısından da tuhaf. Bir tipin hangi interface’i karşılamasının amaçlandığını açıkça göstermek istememek için bir sebep var mı?
Goroutine’lere neden doğrudan thread denmediğini anlamıyorum
Channel’lar neden dil özelliği olmak zorunda? Bence bunun sebebi, generics’in yalnızca üç kadar tip için değil daha genel olarak faydalı olduğunu kabul etmelerinin 10 yıl sürmesi
Channel’ların runtime’ın bir parçası olması da muhtemelen goroutine scheduler’ının channel’ları bilmesi sayesinde, bir channel boş olmaktan çıktığında alıcı goroutine’i daha kolay uyandırabilmesi için. Muhtemelen böyle yapmak daha kolaydı