İçeriğe geç
Muhammet Şafak
Diller 3 dk okuma

Go'da ilk CLI aracımı yazmak

Yeni öğrendiğim Go dilini gerçek bir CLI aracı yazarak pekiştirdim. Dilin pratikteki hissini, standart kütüphaneyle ne kadar uzağa gidilebildiğini anlatıyorum.


Nisan ayında Go öğrenmeye karar verdiğimde “küçük ama gerçek bir şey yazacağım” demiştim. Teorik öğrenme, yani yalnızca dokümantasyon okuyup playground’da denemeler yapmak, bir noktadan sonra kısırlaşıyor. Dili gerçekten anlamak için bir ihtiyacı karşılaması gerekiyor.

Seçtiğim araç basit ama kullandığım bir şey: proje dizinlerini tarayıp .env.example dosyası olan ama .env dosyası olmayan projeleri listeleyen bir CLI. Geliştirme makinemde onlarca proje klasörü var ve zaman zaman bir projeyi açıp “neden çalışmıyor?” diye düşünürken .env dosyasının eksik olduğunu görüyorum. Bu küçük araca ihtiyacım vardı.

Yapı ve paket organizasyonu

Go’da proje organizasyonu PHP’den farklı. main paketi çalıştırılabilir binary üretiyor; başka paketler ise kütüphane. Küçük bir CLI için tek dosya da yeter ama mantığı ayırmayı tercih ettim.

env-checker/
  main.go
  scanner/
    scanner.go

scanner paketi dizin tarama mantığını, main.go ise argüman işleme ve çıktıyı barındırıyor.

Dizin tarama

Go’nun standart kütüphanesi filepath.Walk ile ağaç gezme işini hallediyor. Dış paket yok.

package scanner

import (
    "os"
    "path/filepath"
)

type Result struct {
    Path      string
    HasExample bool
    HasEnv     bool
}

func Scan(root string) ([]Result, error) {
    var results []Result

    entries, err := os.ReadDir(root)
    if err != nil {
        return nil, err
    }

    for _, entry := range entries {
        if !entry.IsDir() {
            continue
        }

        dir := filepath.Join(root, entry.Name())
        hasExample := fileExists(filepath.Join(dir, ".env.example"))
        hasEnv := fileExists(filepath.Join(dir, ".env"))

        if hasExample {
            results = append(results, Result{
                Path:       dir,
                HasExample: true,
                HasEnv:     hasEnv,
            })
        }
    }

    return results, nil
}

func fileExists(path string) bool {
    _, err := os.Stat(path)
    return err == nil
}

PHP’den gelince append fonksiyonu ve nil dilimleri biraz farklı hissettiriyor. Go’da boş bir slice nil; append her çağrıda yeni bir dilim döndürüyor, yerinde değiştirmiyor.

main.go ve çıktı

package main

import (
    "fmt"
    "os"
    "env-checker/scanner"
)

func main() {
    root := "."
    if len(os.Args) > 1 {
        root = os.Args[1]
    }

    results, err := scanner.Scan(root)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Hata: %v\n", err)
        os.Exit(1)
    }

    missing := 0
    for _, r := range results {
        if !r.HasEnv {
            fmt.Printf("EKSIK: %s\n", r.Path)
            missing++
        }
    }

    if missing == 0 {
        fmt.Println("Tüm projelerde .env mevcut.")
    } else {
        fmt.Printf("\n%d projede .env eksik.\n", missing)
    }
}

os.Args ile argüman almak, fmt.Fprintf(os.Stderr, ...) ile standart hata çıktısı vermek — bunların hepsi standart kütüphanede, sıfır bağımlılık.

Derleme ve dağıtım

Beni en çok etkileyen şeylerden biri: go build komutu tek bir çalıştırılabilir dosya üretiyor. PHP’de bir CLI aracı paylaşmak istediğinizde ya Composer paketi yapıyorsunuz ya da PHP kurulu bir ortam gerekiyor. Go ile üretilen ikili, hiçbir bağımlılık olmadan çalışıyor.

go build -o env-checker .
# Oluşan ikili dosyayı /usr/local/bin'e koyup her yerden çağırabilirsiniz

Linux ve macOS için cross-compilation de dahili:

GOOS=linux GOARCH=amd64 go build -o env-checker-linux .

Bu, CLI araçları için Go’nun ciddi bir avantajı. Dağıtmak, paylaşmak, farklı sistemlerde çalıştırmak son derece kolay. Bir araç yapıp başkasına yollamak istediğinizde “Go kurulu mu?” diye sormak zorunda kalmıyorsunuz.

Go’nun bu deneyimden çıkardığım dersleri

err her yerde. Alışmak zaman aldı ama artık anlıyorum: hata akışı explicit olunca ne olabileceğini görmezden gelmek zorlaşıyor. PHP’de bir exception’ı yakalamayı unuttunuz mu? Sessizce kaçabiliyor. Go’da err’i kontrol etmezseniz lint uyarı veriyor, meslektaşlar code review’da fark ediyor.

Standart kütüphane gerçekten zengin. Bu küçük araç için tek dış bağımlılık kullanmadım. Dosya işlemleri, string biçimleme, argüman işleme — hepsi dahili.

Structural typing farklı. Go’da interface’ler açıkça “implement” edilmiyor. Bir struct gerekli metodları içeriyorsa, o interface’i otomatik sağlıyor. PHP’den gelen biri için bu sezgisel değil ama esneklik sağlıyor. scanner.Result tipini bir interface’e bağlamadan, ileride farklı çıktı formatları için aynı veriyi kullanmak mümkün — yapıyı değiştirmeden.

Sürtünmesiz geçti demek istemem. İlk günlerde en çok zorlandığım şey, PHP’deki associative array’lerin Go’da karşılığının olmaması oldu; her veri yapısını önceden bir struct olarak tanımlamak gerekiyor. Başta yavaşlatıcı geldi, ama birkaç gün sonra bunun bir kısıt değil bir disiplin olduğunu fark ettim: verinin şeklini baştan düşünmek zorunda kalıyorsunuz. PHP’de “sonra bir alan daha eklerim” diye bıraktığım belirsizlikleri, Go bana en baştan kapattırdı.

Küçük bir araç ama Go’nun hissini anlamak için yeterliydi. Yeni bir dili gerçek bir ihtiyaç üzerinden öğrenmenin, dokümantasyon okumaktan kat kat verimli olduğunu bir kez daha gördüm. Sonraki adım: dilin concurrency modelini kavramak.

Etiketler: #Go
Paylaş:

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi