go
Go dersleri
Go/Ekosistem ve Pratik

Paketler ve Modüller

Paket yapısı, dışa aktarma, go.mod, bağımlılık yönetimi ve proje düzeni.

Ders 17 / 2120 dkOrta
Bu derste öğreneceklerin
  • package bildirimi ve import
  • Büyük harf ile dışa aktarma (exported)
  • init fonksiyonu
  • go.mod ve go.sum
  • go get, go mod tidy ve sürümleme
  • internal paketleri
  • Proje düzeni ve go work

Tek bir main.go dosyasıyla başlayan her program er ya da geç büyür: fonksiyonlar çoğalır, tipler birbirine bağlanır, başkalarının yazdığı kütüphaneler projeye girer. Bu noktada üç soruya cevap vermen gerekir. Kodu hangi parçalara böleceksin? Her parça dışarıya neyi gösterip neyi saklayacak? Dışarıdan aldığın kodun hangi sürümünü kullandığını nasıl kayıt altına alacaksın?

Go bu soruları iki kavramla cevaplar: paket (package) ve modül (module). Bir halk kütüphanesi düşün. Kitaplar konulara göre raflara ayrılmıştır; bu raflar paketlerdir. Bazı raflar yalnızca personele açıktır; bunlar dışa aktarılmayan tanımlar ve internal dizinleridir. Binanın bir adresi ve bir kayıt defteri vardır; bunlar da modül yolu ve go.mod dosyasıdır. Başka bir kütüphaneden kitap ödünç aldığında hangi baskıyı aldığın deftere yazılır, kitabın yolda değiştirilmediği de bir mühürle doğrulanır. O mühür go.sum dosyasıdır.

Bu dersin sonunda paketlerini doğru adlandırıp düzenleyebilecek, neyi dışa aktaracağına bilinçli karar verebilecek, init fonksiyonlarının hangi sırayla çalıştığını açıklayabilecek, go.mod ve go.sum dosyalarını satır satır okuyabilecek, bağımlılıkları go get ve go mod tidy ile yönetebilecek ve birden fazla modülü go work ile yerelde birlikte geliştirebileceksin.

Paket, modül ve depo

Bu üç kavram sık karıştırılır, o yüzden önce sınırları çizelim:

  • Paket, aynı dizindeki .go dosyalarından oluşan derleme birimidir. Aynı paketteki dosyalar birbirinin tanımlarını import etmeden kullanır; derleyici için hepsi tek bir bütündür.
  • Modül, kök dizininde go.mod dosyası bulunan ve birlikte sürümlenen paketler topluluğudur. Modül yolu (module path) hem modülün kimliğidir hem de içindeki bütün paketlerin import yollarının önekidir.
  • Depo (repository), kodun tutulduğu Git deposudur. Çoğu depo tek bir modül içerir, ama bir depoda birden fazla modül de bulunabilir.
Git deposu: github.com/ayse/banka
└── modül: github.com/ayse/banka              ← kök dizinde go.mod
    ├── paket github.com/ayse/banka               (kök dizin: main.go)
    ├── paket github.com/ayse/banka/account       (account/ dizini)
    │   ├── account.go
    │   └── account_test.go
    └── paket github.com/ayse/banka/internal/money
        └── money.go
KavramNeyi temsil ederNerede tanımlanır
PaketBirlikte derlenen dosyalarHer dosyanın ilk satırı: package ad
ModülBirlikte sürümlenen paketlerKök dizindeki go.mod
Import yoluBir paketin evrensel adıModül yolu + modül içindeki dizin yolu

Standart kütüphane paketlerinin (fmt, net/http, encoding/json) bir modül öneki yoktur. Bunlar Go araç zinciriyle birlikte gelir ve ayrıca indirilmez.

package bildirimi ve import

Paket adı ve import yolu

Her Go dosyası bir package bildirimiyle başlar. Bu satırla ilgili üç kuralı bilmen yeterli:

  1. Bir dizindeki bütün dosyalar aynı paket adını kullanır. Tek istisna, test dosyalarında kullanılabilen _test sonekli dış test paketidir (bkz. Test ve Benchmark).
  2. package main özeldir. Derleyici bu paketi çalıştırılabilir bir programa dönüştürür ve programın giriş noktası func main() olur. Başka her paket adı bir kütüphane üretir.
  3. Import yolu ile paket adı ayrı şeylerdir. import "math/rand/v2" yazarsın ama kodda rand.IntN(10) kullanırsın. Import yolu derleyiciye paketin nerede olduğunu söyler; kodda kullanacağın ad ise o dizindeki dosyaların package satırından gelir. Gelenek, paket adının dizinin son elemanıyla (sürüm soneki hariç) aynı olmasıdır.

import biçimleri

import bildiriminin birkaç biçimi var:

import (
	// Standart import: fmt.Println diye kullanılır.
	"fmt"

	// Kendi modülündeki bir paket: account.New diye kullanılır.
	"example.com/banka/account"

	// Takma ad (alias): paket bu dosyada htmltemplate adıyla anılır.
	htmltemplate "html/template"

	// Boş import: paketin adı kullanılmaz, yalnızca başlatma kodu çalışır.
	_ "image/png"

	// Nokta import: Sqrt(2) yazılabilir. Okunabilirliği bozduğu için kaçın.
	. "math"
)

Takma ad en çok iki paketin adı çakıştığında işe yarar. text/template ve html/template paketlerinin ikisinin de adı template. Aynı dosyada ikisine birden ihtiyacın varsa birine takma ad verirsin:

main.go
package main

import (
	htmltemplate "html/template"
	"os"
	"text/template"
)

func main() {
	const page = "<p>Merhaba, {{.}}</p>\n"
	name := "<b>Ayşe</b>"

	// Metin şablonu veriyi olduğu gibi yazar.
	t := template.Must(template.New("metin").Parse(page))
	if err := t.Execute(os.Stdout, name); err != nil {
		panic(err)
	}

	// HTML şablonu veriyi bağlama göre kaçışlar (escape).
	h := htmltemplate.Must(htmltemplate.New("html").Parse(page))
	if err := h.Execute(os.Stdout, name); err != nil {
		panic(err)
	}
}
Çıktı
<p>Merhaba, <b>Ayşe</b></p>
<p>Merhaba, &lt;b&gt;Ayşe&lt;/b&gt;</p>

İki şablon aynı metni işliyor ama sonuçlar farklı: html/template veriyi HTML bağlamına göre kaçışlıyor, böylece kullanıcıdan gelen <b> etiketi sayfada etiket olarak yorumlanmıyor. Takma ad olmasaydı iki import aynı template adını tanımlamaya çalışacak ve derleme hatası alacaktın.

Go'da kullanılmayan bir import da derleme hatasıdır ("os" imported and not used). Bu kural kodu temiz tutar ama elle takip etmesi yorucudur. Editöründeki gopls ya da goimports aracı importları otomatik ekler, siler ve gruplar.

Paket adlandırma kuralları

Paket adı, o paketi kullanan her satırda görünür: strings.Split, http.Get, json.Marshal. Bu yüzden iyi bir paket adı kısa, açık ve kullanıldığı yerde doğal okunan bir addır.

KaçınTercih etNeden
package userService, package user_servicepackage userPaket adları küçük harfle ve tek kelime olarak yazılır; alt çizgi ve büyük harf kullanılmaz.
package utils, common, helperspackage money, slug, retryAd, paketin ne yaptığını anlatmalı. "Yardımcılar" paketi zamanla ilgisiz her şeyin atıldığı bir çekmeceye dönüşür.
account.NewAccount(), account.AccountTypeaccount.New(), account.TypePaket adı bağlamı zaten veriyor; tekrarlamak (stutter) gereksiz.
v2/ dizininde package v2package paraSürüm soneki import yolunun parçasıdır, paket adının değil.

Bir tuzağa da dikkat et: bir değişkene paketle aynı adı verirsen o fonksiyonda paketi gölgelersin. url := "https://go.dev" yazdığın fonksiyonda artık net/url paketine erişemezsin. Değişkene rawURL ya da u gibi bir ad ver.

Büyük harf ile dışa aktarma

Go'da public ya da private gibi anahtar kelimeler yoktur. Görünürlüğü tanımlayıcının (identifier) ilk harfi belirler:

  • Büyük harfle başlayan ad dışa aktarılır (exported): başka paketler onu kullanabilir.
  • Küçük harfle ya da alt çizgiyle başlayan ad dışa aktarılmaz (unexported): yalnızca tanımlandığı paketin içinden görünür.

Kural istisnasızdır ve her şeye uygulanır: tipler, fonksiyonlar, metotlar, değişkenler, sabitler ve struct alanları. Derleyici aslında ilk karakterin Unicode'daki "büyük harf" kategorisinde olup olmadığına bakar; ama tanımlayıcılarını zaten İngilizce yazacağın için "ilk harf büyük mü?" diye düşünmen yeterli. Görünürlüğün sınırı dosya değil pakettir: account.go içindeki küçük harfli bir fonksiyonu aynı paketteki transfer.go rahatça çağırabilir.

Şimdi iki paketli küçük bir banka modülü kuralım. account paketi bir hesabı modelliyor, internal/money tutarları biçimlendiriyor, kök dizindeki main paketi de ikisini kullanıyor:

banka/
├── go.mod
├── main.go                  package main
├── account/
│   └── account.go           package account
└── internal/
    └── money/
        └── money.go         package money
go.mod
module example.com/banka

go 1.26.2
account/account.go
// Package account, basit bir banka hesabını modeller.
package account

import (
	"errors"
	"fmt"
)

// ErrInsufficientFunds, bakiye çekilmek istenen tutardan az olduğunda döner.
var ErrInsufficientFunds = errors.New("yetersiz bakiye")

// Account bir banka hesabıdır. Bakiye yalnızca metotlarla değiştirilebilir.
type Account struct {
	Owner   string // dışa aktarılır: her paket okuyup yazabilir
	balance int    // dışa aktarılmaz: yalnızca account paketi erişebilir
}

// New, verilen kişi adına sıfır bakiyeli bir hesap oluşturur.
func New(owner string) *Account {
	return &Account{Owner: owner}
}

// Deposit hesaba para yatırır. Tutar pozitif olmalıdır.
func (a *Account) Deposit(amount int) error {
	if err := validate(amount); err != nil {
		return err
	}
	a.balance += amount
	return nil
}

// Withdraw hesaptan para çeker. Bakiye yetmezse ErrInsufficientFunds döner.
func (a *Account) Withdraw(amount int) error {
	if err := validate(amount); err != nil {
		return err
	}
	if amount > a.balance {
		return ErrInsufficientFunds
	}
	a.balance -= amount
	return nil
}

// Balance güncel bakiyeyi döndürür.
func (a *Account) Balance() int {
	return a.balance
}

// validate paketin iç yardımcısıdır; dışarıdan çağrılamaz.
func validate(amount int) error {
	if amount <= 0 {
		return fmt.Errorf("geçersiz tutar: %d", amount)
	}
	return nil
}
internal/money/money.go
// Package money, tutarları Türkçe yazım kurallarıyla biçimlendirir.
package money

import (
	"strconv"
	"strings"
)

// Format, 1250 değerini "1.250 TL" biçimine çevirir.
func Format(amount int) string {
	sign := ""
	if amount < 0 {
		sign = "-"
		amount = -amount
	}
	digits := strconv.Itoa(amount)
	var b strings.Builder
	for i, r := range digits {
		if i > 0 && (len(digits)-i)%3 == 0 {
			b.WriteByte('.')
		}
		b.WriteRune(r)
	}
	return sign + b.String() + " TL"
}
main.go
package main

import (
	"errors"
	"fmt"
	"log"

	"example.com/banka/account"
	"example.com/banka/internal/money"
)

func main() {
	acc := account.New("Ayşe")
	if err := acc.Deposit(1250); err != nil {
		log.Fatal(err)
	}

	if err := acc.Withdraw(2000); errors.Is(err, account.ErrInsufficientFunds) {
		fmt.Println("İşlem reddedildi:", err)
	}
	if err := acc.Deposit(-10); err != nil {
		fmt.Println("İşlem reddedildi:", err)
	}

	fmt.Printf("%s: %s\n", acc.Owner, money.Format(acc.Balance()))
}

Modülün kök dizininde programı çalıştıralım:

go run .
Çıktı
İşlem reddedildi: yetersiz bakiye
İşlem reddedildi: geçersiz tutar: -10
Ayşe: 1.250 TL

Neler olduğuna adım adım bakalım:

  • account.New, account.ErrInsufficientFunds, Account tipi ve Owner alanı büyük harfle başladığı için main paketinden erişilebilir.
  • balance alanı ve validate fonksiyonu küçük harfle başlıyor; onlara yalnızca account paketi dokunabilir. Bakiyeyi değiştirmenin tek yolu Deposit ve Withdraw metotları ve ikisi de tutarı doğruluyor. Böylece "bakiye negatif olamaz" kuralını paketin kendisi garanti ediyor; paketi kullanan kimse bu kuralı yanlışlıkla çiğneyemiyor.
  • Balance() metodu alanı salt okunur biçimde dışarı açıyor. Go'da okuma metotlarına GetBalance değil, doğrudan Balance adı verilir.

Peki main paketi kuralı çiğnemeye çalışırsa ne olur?

main.go
package main

import (
	"fmt"

	"example.com/banka/account"
)

func main() {
	acc := account.New("Ayşe")
	acc.balance = 1_000_000
	fmt.Println(account.validate(5))
}
go build .
Çıktı
# example.com/banka
./main.go:11:6: acc.balance undefined (type *account.Account has no field or method balance, but does have method Balance)
./main.go:12:22: name validate not exported by package account

Derleyici iki ihlali de yakalıyor, ilkinde yakın bir ad önererek (but does have method Balance) yardımcı da oluyor. Dışa aktarma bir öneri ya da lint kuralı değil, dilin kendisinin koyduğu bir sınırdır.

Neyi dışa aktarmalı?

Dışa aktardığın her ad, paketini kullananlara verdiğin bir sözdür. Bir fonksiyonu dışa aktarıp başkalarının onu kullanmasına izin verdikten sonra imzasını değiştirmek, onların kodunu bozmak demektir. Dışa aktarılmayan bir adı ise istediğin gibi değiştirebilir, silebilir, yeniden adlandırabilirsin. Bu yüzden iyi bir alışkanlık şudur: her şeyi küçük harfle başlat, yalnızca paketin kullanıcılarının gerçekten ihtiyaç duyduğu şeyleri büyük harfe çevir. Küçük bir API hem öğrenmesi kolay hem de bakımı ucuzdur.

init fonksiyonu

Bir paketin kullanıma hazır hale gelmesi iki adımda olur: önce paket düzeyindeki (package-level) değişkenlere ilk değerleri verilir, sonra init fonksiyonları çalışır. init fonksiyonunun kendine özgü kuralları var:

  • Parametre almaz ve değer döndürmez: func init().
  • Kodun hiçbir yerinden çağrılamaz, adı bir değer olarak da kullanılamaz.
  • Bir dosyada birden fazla, bir pakette istediğin kadar init bulunabilir.
  • Paket kaç farklı yerden import edilirse edilsin başlatma işlemi bir kez yapılır.
  • Bütün başlatma işlemleri tek bir goroutine'de, sırayla ve main fonksiyonundan önce yapılır.

Bir paketin içinde başlatma sırası

Paket düzeyi değişkenler yazılış sırasıyla değil, bağımlılık sırasıyla başlatılır: bir değişkenin ilk değeri başka bir değişkene bağlıysa önce o değişken hazırlanır. Bağımlılığı olmayan değişkenler yazıldıkları sırayı izler. Bütün değişkenler hazır olduktan sonra init fonksiyonları kaynak koddaki sıralarıyla çalışır. Paket birden çok dosyadan oluşuyorsa go aracı dosyaları adlarına göre sıralayıp derleyiciye verir; bu yüzden a.go dosyasındaki init, b.go dosyasındakinden önce çalışır.

main.go
package main

import "fmt"

// total ilk sırada yazıldı ama price ve tax'a bağlı olduğu için
// onlardan sonra hesaplanır.
var total = add(price, tax)

var price = trace("price", 100)
var tax = trace("tax", 18)

func trace(name string, v int) int {
	fmt.Printf("değişken başlatılıyor: %s = %d\n", name, v)
	return v
}

func add(a, b int) int {
	fmt.Println("değişken başlatılıyor: total")
	return a + b
}

func init() {
	fmt.Println("birinci init, total =", total)
}

func init() {
	fmt.Println("ikinci init")
}

func main() {
	fmt.Println("main başladı")
}
Çıktı
değişken başlatılıyor: price = 100
değişken başlatılıyor: tax = 18
değişken başlatılıyor: total
birinci init, total = 118
ikinci init
main başladı

total ilk sırada yazılmış olsa da price ve tax değişkenlerine bağlı olduğu için onlardan sonra hesaplandı. İki init fonksiyonu da bütün değişkenler hazır olduktan sonra, yazıldıkları sırayla çalıştı; main en son geldi.

Paketler arasında başlatma sırası

Paketler arasında tek bir temel kural vardır: bir paket, import ettiği bütün paketler başlatıldıktan sonra başlatılır. Aynı anda birden fazla paket başlatılmaya hazır olduğunda hangisinin önce geleceğini Go 1.21 ile kesinleştirilen bir algoritma belirler: paketler import yollarına göre alfabetik sıralanır; sonra listede, import ettiği her şey başlatılmış olan ilk paket seçilir, başlatılır ve listeden çıkarılır. Bu işlem liste boşalana kadar tekrarlanır.

Bunu gözle görmek için dört paketli bir modül kuralım: main paketi a ve b paketlerini, a da c paketini import ediyor. Her paket, değişkeni başlatılırken ve init fonksiyonu çalışırken ekrana bir satır yazıyor. Modülün yolu example.com/initsira; a, b ve c paketleri aynı adlı alt dizinlerde duruyor. b ve c, a ile aynı kalıpta yazıldı; tek farkları başka bir paket import etmemeleri.

a/a.go
package a

import (
	"fmt"

	"example.com/initsira/c"
)

var Ready = c.Ready && announce("a: paket düzeyi değişken")

func init() {
	fmt.Println("a: init")
}

func announce(msg string) bool {
	fmt.Println(msg)
	return true
}
main.go
package main

import (
	"fmt"

	"example.com/initsira/a"
	"example.com/initsira/b"
)

var ready = a.Ready && b.Ready && announce("main: paket düzeyi değişken")

func init() {
	fmt.Println("main: init")
}

func announce(msg string) bool {
	fmt.Println(msg)
	return true
}

func main() {
	fmt.Println("main: başladı, ready =", ready)
}
go run .
Çıktı
b: paket düzeyi değişken
b: init
c: paket düzeyi değişken
c: init
a: paket düzeyi değişken
a: init
main: paket düzeyi değişken
main: init
main: başladı, ready = true

main.go içinde a paketi b'den önce import edildiği halde neden b en önce başladı? Algoritmayı adım adım uygulayalım (standart kütüphane paketlerini sadelik için dışarıda bırakıyoruz):

import grafiği        import yoluna göre sıralı liste

      main            example.com/initsira       (main)  a ve b'yi bekliyor
      /  \            example.com/initsira/a             c'yi bekliyor
     a    b           example.com/initsira/b             hazır
     |                example.com/initsira/c             hazır
     c

1. tur: main bekliyor, a bekliyor, b hazır     → b başlatılır
2. tur: main bekliyor, a bekliyor, c hazır     → c başlatılır
3. tur: main bekliyor (a eksik), a hazır       → a başlatılır
4. tur: main hazır                             → main başlatılır, sonra main() çalışır

Sıra, import satırlarının yazılış sırasına göre değil, bu algoritmaya göre belirlendi. Her durumda değişmeyen şey şu: a, kendisinden önce c'nin hazır olduğunu bilir; main de hem a'nın hem b'nin hazır olduğunu bilir.

init ne zaman kullanılır?

init iki durumda gerçekten işe yarar:

  • Hazırlanması birkaç satır süren paket düzeyi veriler. Bir arama tablosunu doldurmak ya da bir tabloyu başka bir tablodan türetmek gibi.
  • Kayıt (registration) deseni. Bir paket init içinde kendini başka bir pakete kaydeder. Örneğin image/png paketi PNG çözücüsünü init içinde image paketine kaydeder. Bu yüzden programında png adını hiç kullanmasan bile _ "image/png" boş importu, image.Decode fonksiyonunun PNG dosyalarını tanımasını sağlar. database/sql için yazılmış veritabanı sürücüleri de aynı yolla kaydolur.

Modüller: go.mod ve go.sum

go mod init

Yeni bir modül, boş bir dizinde go mod init example.com/banka komutuyla başlar. Komut, argüman olarak verdiğin modül yolunu ve kullandığın Go sürümünü içeren kısa bir go.mod dosyası oluşturur.

Modülünü başkalarının go get ile indirmesini istiyorsan modül yolu, kodun gerçekten bulunduğu depo adresi olmalıdır: github.com/ayse/banka gibi. Go aracı kodu bu yoldan yola çıkarak bulur. Yalnızca yerelde deneme yapıyorsan example.com/... biçiminde bir yol yeterli; example.com alan adı zaten örnekler için ayrılmıştır.

go.mod satır satır

Aşağıdaki go.mod, iki doğrudan bağımlılığı olan ve kod üretmek için stringer aracını kullanan bir modülden alındı:

go.mod
module example.com/banka

go 1.26.2

require (
	github.com/google/uuid v1.6.0
	golang.org/x/text v0.42.0
)

require (
	golang.org/x/mod v0.41.0 // indirect
	golang.org/x/sync v0.23.0 // indirect
	golang.org/x/tools v0.50.0 // indirect
)

tool golang.org/x/tools/cmd/stringer
SatırAnlamı
module example.com/bankaModül yolu. İçindeki paketlerin import yolları bununla başlar.
go 1.26.2Modülün gerektirdiği en düşük Go sürümü. Go 1.21'den beri bu kesin bir gerekliliktir: daha eski bir araç zinciri modülü derlemez (ya da GOTOOLCHAIN ayarına göre gereken sürümü indirir). Dil özellikleri de bu sürüme göre etkinleşir.
İlk require bloğuKodunun doğrudan import ettiği modüller ve kabul edilebilir en düşük sürümleri.
// indirectKodunun doğrudan import etmediği ama derleme için gereken modüller. Burada stringer aracının bağımlılıkları.
tool golang.org/x/tools/cmd/stringerGo 1.24 ile gelen tool yönergesi. Aracı go tool stringer ile, modülde sabitlenmiş sürümüyle çalıştırırsın.

Her projede görmesen de tanıman gereken birkaç yönerge daha var:

YönergeNe işe yarar
toolchain go1.26.2Modülle çalışırken önerilen araç zinciri sürümü.
replace eski => yeniBir modülü başka bir sürümle ya da yerel bir dizinle değiştirir: replace example.com/para => ../para. Yalnızca ana modülde etkilidir; seni import edenleri etkilemez.
exclude modül sürümBelirli bir sürümün seçilmesini engeller.
retract v1.0.1Modül yazarının, yayımladığı hatalı bir sürümü "kullanmayın" diye işaretlemesi.
godebug anahtar=değerGODEBUG davranış ayarlarının varsayılanlarını modül düzeyinde belirler.

go.sum

Aynı modülün go.sum dosyası şöyle:

go.sum
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=

Her satır üç parçadan oluşur: modül yolu, sürüm ve bir özet (hash). Her modül için iki satır görürsün. h1: ile başlayan ilk satır, o sürümdeki bütün dosyaların SHA-256 tabanlı özetidir. Sürümün sonuna /go.mod eklenmiş ikinci satır ise yalnızca o modülün go.mod dosyasının özetidir. İkincisi ayrı tutulur çünkü Go, bağımlılık grafiğini çözerken birçok modülün yalnızca go.mod dosyasını okur, kaynak kodunu hiç indirmez.

Bir modül sürümünü ilk kez indirdiğinde Go, hesapladığı özeti sum.golang.org sağlama toplamı veritabanıyla (checksum database) karşılaştırır. Biri bir sürüm etiketini taşıyıp içeriğini sessizce değiştirse bile bu sayede fark edilir. Sonraki derlemelerde yerel önbellekteki dosyalar da go.sum ile doğrulanır. Şirket içi, herkese açık olmayan modüllerin bu veritabanına sorulmaması için GOPRIVATE ortam değişkenini kullanırsın (GOPRIVATE=github.com/sirketim/*).

Minimal sürüm seçimi (MVS)

require satırındaki sürüm "tam olarak bu" değil, "en az bu" anlamına gelir. Birden fazla modül aynı bağımlılığın farklı sürümlerini isteyebilir. Go bu durumda Minimal Sürüm Seçimi (Minimal Version Selection, MVS) algoritmasını kullanır: her bağımlılık için, gereksinim grafiğinde istenen sürümlerin en büyüğünü seçer. Yayımlanmış en yeni sürüme bakmaz.

senin modülün ──require──▶ A v1.2.0

      └──require──▶ B v1.0.0 ──require──▶ A v1.4.0

A'nın yayımlanmış sürümleri:  v1.2.0   v1.3.0   v1.4.0   v1.5.0   v1.7.0

                              seçilen: istenenlerin en büyüğü = v1.4.0

Senin modülün "en az v1.2.0", B ise "en az v1.4.0" diyor. İkisini de mutlu eden en küçük sürüm v1.4.0. v1.7.0 yayımlanmış olsa da kimse onu istemediği için seçilmiyor. Bu yaklaşımın güzel yanı öngörülebilir olmasıdır: bir kütüphane yazarı bugün yeni bir sürüm yayımladığında senin derlemen yarın kendiliğinden değişmez. Yükseltme her zaman senin bilinçli bir go get komutunla olur.

go get, go mod tidy ve sürümleme

Anlamsal sürümleme

Go modülleri anlamsal sürümleme (semantic versioning, kısaca semver) kullanır. Bir sürüm vMAJOR.MINOR.PATCH biçimindedir:

ParçaNe zaman artarÖrnek
MAJORGeriye uyumsuz bir API değişikliğindev1.9.3v2.0.0
MINORGeriye uyumlu yeni bir özellik eklendiğindev1.4.2v1.5.0
PATCHGeriye uyumlu bir hata düzeltmesindev1.4.2v1.4.3
  • v0.x.y sürümleri kararsız kabul edilir; her minor sürümde API değişebilir.
  • v2.0.0-rc.1 gibi ön sürümler (pre-release), kararlı bir sürüm varken @latest ile seçilmez.
  • Etiketlenmemiş bir commit'e bağımlı olursan Go bir sahte sürüm (pseudo-version) üretir: v0.0.0-20260105143000-1a2b3c4d5e6f. Bu dizge bir taban sürüm, UTC zaman damgası ve commit özetinin ilk 12 karakterinden oluşur.

Bağımlılık ekleme ve güncelleme

Gerçek bir örnek üzerinden gidelim. Türkçe kelimeleri sıralamak istiyoruz. slices.Sort dizgeleri byte değerlerine göre sıralar; ç, ı, ö, ş, ü gibi harflerin UTF-8 kodları ASCII harflerinden büyük olduğu için bu harflerle başlayan kelimeler en sona düşer. Doğru alfabetik sıra için Go ekibinin bakımını yaptığı golang.org/x/text modülündeki collate paketini kullanacağız:

main.go
package main

import (
	"fmt"
	"slices"

	"golang.org/x/text/collate"
	"golang.org/x/text/language"
)

func main() {
	words := []string{"zeytin", "şeftali", "çilek", "ıhlamur", "incir", "ördek", "elma", "üzüm"}

	byteOrder := slices.Clone(words)
	slices.Sort(byteOrder)
	fmt.Println("slices.Sort:", byteOrder)

	c := collate.New(language.Turkish)
	c.SortStrings(words)
	fmt.Println("collate    :", words)
}

Bağımlılığı önce bilerek eski bir sürümle ekleyelim, sonra en yeni sürüme yükseltelim:

go get golang.org/x/[email protected]
go get golang.org/x/text@latest
Çıktı
go: downloading golang.org/x/text v0.20.0
go: added golang.org/x/text v0.20.0
go: downloading golang.org/x/text v0.42.0
go: upgraded golang.org/x/text v0.20.0 => v0.42.0

go.mod dosyasına bakınca bir tuhaflık görürsün:

go.mod
module example.com/sirala

go 1.26.2

require golang.org/x/text v0.42.0 // indirect

main.go bu modülü doğrudan import ettiği halde satırda // indirect yazıyor. go get bağımlılığı ekler ama işaretlemeyi her zaman doğru yapmaz. Modül dosyasını kodla uyumlu hale getiren komut go mod tidy:

go mod tidy
cat go.mod
go run .
Çıktı
module example.com/sirala

go 1.26.2

require golang.org/x/text v0.42.0
slices.Sort: [elma incir zeytin çilek ördek üzüm ıhlamur şeftali]
collate    : [çilek elma ıhlamur incir ördek şeftali üzüm zeytin]

go mod tidy, modüldeki bütün paketleri (testler ve farklı işletim sistemleri için olan dosyalar dahil) tarar. Eksik gereksinimleri ekler, artık kullanılmayanları siler, // indirect işaretlerini düzeltir ve go.sum dosyasını günceller. Bir değişikliği commit etmeden önce çalıştırmayı alışkanlık haline getir. CI tarafında go mod tidy -diff komutu dosyaları değiştirmeden farkı gösterir ve düzenlenecek bir şey varsa hata koduyla çıkar.

go get komutunun sık kullanılan biçimleri şunlar:

KomutEtkisi
go get example.com/[email protected]Belirli bir sürüme geç (yükseltme ya da düşürme).
go get example.com/pkg@latestEn yeni kararlı sürüme geç.
go get example.com/pkg@patchŞu anki minor sürümün en yeni yamasına geç.
go get example.com/pkg@noneBağımlılığı kaldır.
go get example.com/pkg@a1b2c3dBelirli bir commit'e bağlan (sahte sürüm üretilir).
go get -u ./...Modüldeki paketlerin bağımlılıklarını en yeni minor veya patch sürümlerine yükselt.
go get -u=patch ./...Yalnızca patch yükseltmelerini yap.

Modülleri incelemek için de şu komutları kullanırsın:

KomutNe gösterir / ne yapar
go list -m allDerlemede seçilmiş bütün modül sürümlerini listeler.
go list -m -u allHangi bağımlılıkların daha yeni sürümü olduğunu gösterir.
go list -m -versions golang.org/x/textBir modülün yayımlanmış bütün sürümlerini listeler.
go mod why golang.org/x/text/languageBir paketin neden gerektiğini, import zinciriyle açıklar.
go mod graphGereksinim grafiğini satır satır yazdırır.
go mod verifyÖnbellekteki modüllerin indirildikten sonra değişmediğini doğrular.

Büyük sürümler ve /v2 import yolu

Go'nun sürümleme sistemi, import uyumluluk kuralına (import compatibility rule) dayanır: eski bir paketle yeni bir paket aynı import yolunu paylaşıyorsa, yeni paket eskisiyle geriye uyumlu olmalıdır. Major sürüm ise tanımı gereği uyumluluğu bozar. O halde yeni major sürümün yeni bir import yolu olmalıdır. Buna anlamsal import sürümleme (semantic import versioning) denir.

Pratikte v2 ve sonrasında modül yolunun sonuna major sürüm eklenir:

// para modülünün go.mod dosyasındaki satır:
//     module github.com/ayse/para/v2
//
// Bu modülü kullanan kod:
import "github.com/ayse/para/v2" // kodda yine para.Format diye kullanılır

Kazanç büyük: github.com/ayse/para ile github.com/ayse/para/v2 Go'nun gözünde iki ayrı modüldür ve aynı programda yan yana kullanılabilir. Büyük bir projede v2'ye tek seferde değil, paket paket geçebilirsin. Standart kütüphane de bu yolu izledi: Go 1.22 ile gelen math/rand/v2, eski math/rand paketini bozmadan yanına eklendi.

Bir v2 modülünü iki şekilde düzenleyebilirsin: ya depo kökündeki go.mod dosyasının module satırını /v2 ile bitirip v2.0.0 etiketi atarsın, ya da deponda kendi go.mod dosyası olan bir v2/ alt dizini açarsın. v0 ve v1 sürümleri sonek almaz. Kendi go.mod dosyası olan bir modül, /v2 soneki olmadan v2 etiketi yayımlarsa Go bu sürümü kabul etmez. Modül sistemi öncesinden kalan, go.mod dosyası olmayan eski depolardaki v2 ve üstü etiketler ise v2.3.0+incompatible gibi görünür.

internal paketleri

Dışa aktarma kuralı iki kademe sunar: paketin içi ve bütün dünya. Oysa büyük bir modülde çoğu zaman üçüncü bir kademe istersin: "modülümdeki paketler kullanabilsin ama dışarıdakiler kullanamasın". Bunun için yolunda internal adlı bir dizin bulunan paketler kullanılır.

Kural şudur: .../a/b/internal/c yolundaki bir paketi yalnızca .../a/b dizininin altındaki paketler import edebilir.

example.com/banka/
├── main.go                     ✓ internal/money'i import edebilir
├── account/
│   ├── account.go              ✓ internal/money'i import edebilir
│   └── internal/rates/         yalnızca account/ altındakiler kullanabilir
│                               (kök dizindeki main.go bile kullanamaz)
└── internal/
    └── money/money.go

example.com/baska/main.go       ✗ example.com/banka/internal/money yasak

Başka bir modülden internal/money paketini import etmeye çalışalım:

baska/main.go
package main

import (
	"fmt"

	"example.com/banka/internal/money"
)

func main() {
	fmt.Println(money.Format(5000))
}
go build ./baska
Çıktı
package example.com/baska
	baska/main.go:6:2: use of internal package example.com/banka/internal/money not allowed

Bu kuralı go aracı uygular; bir gelenek ya da uyarı değil, derlemeyi durduran bir hatadır. internal dizinleri sayesinde modülünün dışarıya açık yüzeyini küçük tutarken, iç paketlerini birbirleriyle serbestçe paylaşabilir ve istediğin zaman yeniden düzenleyebilirsin. Kimse onlara bağımlı olamayacağı için değiştirmek kimsenin kodunu bozmaz. internal dizini yolun herhangi bir derinliğinde bulunabilir; sınır her zaman internal dizininin bir üst dizinidir.

Proje düzeni

Go projeleri için dayatılmış tek bir dizin yapısı yoktur ve küçük başlamak en iyisidir. Tek bir paketten oluşan bir araç için kök dizinde go.mod ve birkaç .go dosyası yeter. Proje büyüdükçe şu yapı yaygın ve kullanışlıdır:

banka/
├── go.mod
├── go.sum
├── cmd/
│   ├── banka-api/
│   │   └── main.go            HTTP sunucusu (package main)
│   └── banka-cli/
│       └── main.go            komut satırı aracı (package main)
├── internal/
│   ├── account/               iş kuralları
│   ├── money/
│   └── storage/               veritabanı erişimi
└── testdata/                  testlerin okuduğu dosyalar
  • cmd/ altındaki her dizin ayrı bir package main programıdır. go build ve go install, çalıştırılabilir dosyaya dizinin adını verir: cmd/banka-cli dizininden banka-cli programı çıkar.
  • internal/ altındaki paketler uygulamanın kendisidir ve dışarıya kapalıdır.
  • Başkalarının import etmesi için yazdığın bir kütüphanede ise ana paketi modülün kök dizinine koyarsın: github.com/ayse/slug modülünün kökünde slug.go bulunur, kullanıcılar slug.Make(...) yazar.
  • Paketlere tiplere göre (models/, controllers/, services/) değil, sorumluluklara göre (account, money, storage) ayırmak Go'da daha doğal sonuç verir. Bir pkg/ dizini de zorunlu değildir; Go ekibinin rehberi için go.dev/doc/modules/layout sayfasına bakabilirsin.

Bu yapıyla çalışırken sık kullanacağın komutlar:

go build ./...                 # modüldeki bütün paketleri derle
go test ./...                  # bütün paketlerin testlerini çalıştır
go vet ./...                   # bütün paketleri statik analizden geçir
go run ./cmd/banka-cli         # tek bir programı derleyip çalıştır
go install ./cmd/banka-cli     # programı $(go env GOPATH)/bin altına kur

./... deseni "bu dizin ve altındaki bütün paketler" demektir. go install, derlenen programı GOBIN ortam değişkeninin gösterdiği dizine, o tanımlı değilse $(go env GOPATH)/bin altına koyar; bu dizini PATH değişkenine eklersen programı her yerden çalıştırabilirsin. Başkasının yazdığı bir aracı kurmak için sürümle birlikte tam yolunu verirsin: go install golang.org/x/tools/cmd/stringer@latest. Sürümlü go install bulunduğun dizindeki go.mod dosyasını yok sayar ve modülünü değiştirmez.

go work ile çoklu modül geliştirme

Diyelim ki iki modül üzerinde aynı anda çalışıyorsun: example.com/metin adında bir kütüphane ve onu kullanan example.com/uygulama. Kütüphaneye yeni bir fonksiyon ekledin ve uygulamada hemen denemek istiyorsun, ama kütüphaneyi henüz yayımlamadın. Uygulamanın dizininde çalıştırınca Go modülü bulamaz:

calisma/
├── metin/
│   ├── go.mod          module example.com/metin
│   └── metin.go
└── uygulama/
    ├── go.mod          module example.com/uygulama
    └── main.go
metin/metin.go
// Package metin, Türkçe metinler için yardımcı fonksiyonlar sunar.
package metin

import (
	"strings"
	"unicode"
	"unicode/utf8"
)

// Title her kelimenin ilk harfini Türkçe kurallarına göre büyütür.
func Title(s string) string {
	words := strings.Fields(s)
	for i, w := range words {
		r, size := utf8.DecodeRuneInString(w)
		words[i] = string(unicode.TurkishCase.ToUpper(r)) + w[size:]
	}
	return strings.Join(words, " ")
}
uygulama/main.go
package main

import (
	"fmt"

	"example.com/metin"
)

func main() {
	fmt.Println(metin.Title("ılık bir istanbul akşamı"))
}
cd uygulama
go run .
Çıktı
main.go:6:2: no required module provides package example.com/metin; to add it:
	go get example.com/metin

Eskiden bu sorun go.mod dosyasına replace example.com/metin => ../metin satırı eklenerek çözülürdü. İşe yarar, ama o satırı commit etmeyi unutmazsan başkalarının derlemesi bozulur. Go 1.18 ile gelen çalışma alanları (workspaces) bu işi modül dosyalarına dokunmadan yapar:

cd ..
go work init ./metin ./uygulama
cat go.work
go run ./uygulama
Çıktı
go 1.26.2

use (
	./metin
	./uygulama
)
Ilık Bir İstanbul Akşamı

go.work dosyası, listelenen modüllerin hepsini aynı anda "ana modül" olarak kullanır. uygulama artık example.com/metin paketini doğrudan yan dizinden alıyor; ne go get ne de replace gerekti. Bu dosyanın bulunduğu dizinde ya da alt dizinlerinde çalıştırdığın her go komutu çalışma alanı modunda çalışır.

KomutNe yapar
go work init ./a ./bgo.work dosyasını oluşturur ve modülleri ekler.
go work use ./cÇalışma alanına bir modül daha ekler.
go work use -r .Alt dizinlerdeki bütün modülleri bulup ekler.
go work syncÇalışma alanında seçilen bağımlılık sürümlerini modüllerin go.mod dosyalarına geri yazar.
GOWORK=off go build ./...Çalışma alanını geçici olarak devre dışı bırakır.

Belgelendirme: go doc ve pkg.go.dev

Go'da belgeler ayrı bir dosyada değil, kodun hemen üstündeki yorumlarda yaşar. Bir tanımın hemen önündeki yorum onun belge yorumudur (doc comment). Gelenekler basit:

  • Yorum, tanımlanan adla başlayan tam bir cümledir: // Withdraw hesaptan para çeker.
  • Paket yorumu // Package account, ... diye başlar ve package satırının hemen üstüne yazılır. Uzun paket belgeleri için doc.go adında ayrı bir dosya açmak yaygındır.
  • Belge yorumlarında başlık (# Başlık), liste ve [account.New] biçiminde tanımlara bağlantı kullanılabilir.

go doc komutu bu yorumları terminalde gösterir. Banka modülünün kök dizininde:

go doc ./account
go doc ./account Account.Withdraw
Çıktı
package account // import "example.com/banka/account"

Package account, basit bir banka hesabını modeller.

var ErrInsufficientFunds = errors.New("yetersiz bakiye")
type Account struct{ ... }
    func New(owner string) *Account
package account // import "example.com/banka/account"

func (a *Account) Withdraw(amount int) error
    Withdraw hesaptan para çeker. Bakiye yetmezse ErrInsufficientFunds döner.

Dikkat edersen validate fonksiyonu ve balance alanı listede yok: go doc varsayılan olarak yalnızca dışa aktarılmış adları gösterir (-u bayrağı hepsini gösterir). Birkaç kullanışlı biçim daha: go doc fmt.Println tek bir fonksiyonu, go doc -all net/http bir paketin bütün belgesini, go doc -src strings.Cut kaynak kodu gösterir.

Herkese açık modüllerin belgeleri pkg.go.dev sitesinde yayımlanır. Bir modül sürümü Go modül vekil sunucusu (proxy.golang.org) üzerinden bir kez istendiğinde site onu kendiliğinden işler. Sayfada belgelerin yanında modülün sürümleri, lisansı, bağımlılıkları ve onu import eden paketler de görünür. Test dosyalarına yazdığın Example fonksiyonları da burada çalıştırılabilir örnek olarak listelenir.

Sık yapılan hatalar

  • Döngüsel import. x paketi y'yi, y de x'i import ederse derleme import cycle not allowed hatasıyla durur. Go döngüsel bağımlılıklara izin vermez. Ortak tipleri üçüncü bir pakete taşı ya da bağımlılığı bir arayüzle ters çevir: arayüzü, onu kullanan pakette tanımla.
  • Dizin adıyla paket adının farklı olması. money/ dizininde package para yazmak derlenir ama import eden herkesi şaşırtır; import satırı money derken kodda para.Format görünür. Takma ad kullanmıyorsan paket adını dizin adıyla aynı tut.
  • Her şeyi utils ya da common paketine koymak. Bu paketler zamanla herkesin bağımlı olduğu, kimsenin sahiplenmediği yığınlara dönüşür ve döngüsel import sorunlarına davetiye çıkarır. Kodu ne yaptığına göre adlandırılmış küçük paketlere böl.
  • JSON'da küçük harfli alanların görünmesini beklemek. Dışa aktarılmayan alanlar yansıma kullanan paketler için görünmezdir.
  • go.mod dosyasını elle düzenleyip go mod tidy çalıştırmamak ya da go.sum dosyasını commit etmemek. Başkasının makinesinde derleme missing go.sum entry hatasıyla durur.
  • v2 yayımlarken /v2 sonekini unutmak. Major sürümü yükselttiğinde modül yolunu ve kendi içindeki bütün importları da güncellemen gerekir.
  • Modül yolunun depo adresiyle uyuşmaması. go.mod içinde module banka yazıp kodu github.com/ayse/banka adresinde yayımlarsan, go get bu modülü indirirken module declares its path as diye başlayan bir hatayla reddeder.
  • Yerel replace satırlarını commit etmek. replace example.com/metin => ../metin senin makinende çalışır, başkasınınkinde çalışmaz. Yerel geliştirme için go work kullan.
  • init fonksiyonuna iş yüklemek. Hata döndüremeyen, test edilmesi zor ve her import edende çalışan gizli kod yazmış olursun.

Alıştırmalar

Alıştırma·İsim çakışmasını takma adla çöz
Kolay

Aynı programda hem eski math/rand hem de yeni math/rand/v2 paketini kullan. İki paketin de adı rand olduğu için birine takma ad vermen gerekecek. Her paketten sabit tohumlu (seed) bir üreteç oluştur ve 0 ile 99 arasında üçer sayı yazdır. Eski pakette tohum olarak 42, yeni pakette rand.NewPCG(1, 2) kullan.

İpucu

Eski paket rand.New(rand.NewSource(42)) ve Intn metodunu, yeni paket rand.New(rand.NewPCG(1, 2)) ve IntN metodunu kullanır. Takma adı import satırında, yolun önüne yazarsın.

Çözümü göster
main.go
package main

import (
	"fmt"
	randv1 "math/rand"
	"math/rand/v2"
)

func main() {
	old := randv1.New(randv1.NewSource(42))
	fmt.Println("math/rand   :", old.Intn(100), old.Intn(100), old.Intn(100))

	r := rand.New(rand.NewPCG(1, 2))
	fmt.Println("math/rand/v2:", r.IntN(100), r.IntN(100), r.IntN(100))
}
Çıktı
math/rand   : 5 87 68
math/rand/v2: 76 61 78

math/rand/v2 farklı bir import yoluna sahip olduğu için Go'nun gözünde tamamen ayrı bir pakettir; iki sürüm aynı programda sorunsuzca yan yana durur. Paket adları çakıştığı için eski pakete randv1 takma adını verdik. Metot adlarındaki fark (Intn ve IntN), major sürüm yükseltmesinin neden yeni bir import yolu gerektirdiğini gösteriyor: v2 geriye uyumsuz API değişiklikleri içeriyor.

Alıştırma·Başlatma sırası hatasını bul
Orta

Aşağıdaki program işlemleri bir haritaya kaydediyor ve kaç işlem kayıtlı olduğunu yazdırıyor. Çıktıda kayıtlı işlem sayısı: 2 görmeyi bekliyoruz ama program 0 yazdırıyor. Nedenini açıkla ve summary değişkenini paket düzeyinde bırakarak programı düzelt.

main.go
package main

import "fmt"

var operations = map[string]func(a, b int) int{}

var summary = fmt.Sprintf("kayıtlı işlem sayısı: %d", len(operations))

func init() {
	operations["topla"] = func(a, b int) int { return a + b }
	operations["çarp"] = func(a, b int) int { return a * b }
}

func main() {
	fmt.Println(summary)
	fmt.Println("topla(3, 4) =", operations["topla"](3, 4))
}
Çıktı
kayıtlı işlem sayısı: 0
topla(3, 4) = 7
İpucu

Paket düzeyi değişkenlerin hepsi, herhangi bir init fonksiyonu çalışmadan önce başlatılır. summary hesaplandığı anda harita hâlâ boştur. Haritayı doldurma işini değişken başlatmanın bir parçası yaparsan bağımlılık sırası işini görür.

Çözümü göster
main.go
package main

import "fmt"

// summary, operations'a bağlı olduğu için yukarıda yazılmış olsa da
// operations hazırlandıktan sonra hesaplanır.
var summary = fmt.Sprintf("kayıtlı işlem sayısı: %d", len(operations))

var operations = map[string]func(a, b int) int{
	"topla": func(a, b int) int { return a + b },
	"çarp":  func(a, b int) int { return a * b },
}

func main() {
	fmt.Println(summary)
	fmt.Println("topla(3, 4) =", operations["topla"](3, 4))
}
Çıktı
kayıtlı işlem sayısı: 2
topla(3, 4) = 7

Hatalı programda sıra şuydu: önce operations boş bir harita olarak, sonra summary o boş haritanın uzunluğuyla (0) başlatıldı; init haritayı ancak bunlardan sonra doldurdu. Düzeltmede kayıtları haritanın bileşik değişmezine (composite literal) taşıdık. Artık summary, dolu haritaya bağlı bir değişken; derleyici bağımlılığı gördüğü için summary değişkenini, yukarıda yazılmış olsa bile operations hazır olduktan sonra hesaplıyor. Diğer bir çözüm, summary değerini init içinde, kayıtlardan sonra atamak olurdu.

Alıştırma·cmd ve internal düzeniyle kelime sayacı
Orta

example.com/sayac adında bir modül oluştur. Modülde iki paket olsun:

  • internal/counter: kelime frekanslarını tutan bir Counter tipi. Haritası dışa aktarılmasın. Add(word string), Count(word string) int ve en sık geçen n kelimeyi döndüren Top(n int) []string metotları olsun (eşitlikte alfabetik sıra). Counter tipinin sıfır değeri, yapıcı fonksiyon çağırmadan kullanılabilmeli.
  • cmd/sayac: bir cümledeki kelimeleri sayıp sonuçları yazdıran program.

Her dışa aktarılmış ada belge yorumu yaz ve go doc ile kontrol et.

İpucu

Sıfır değerin kullanılabilir olması için Add içinde harita nil ise önce make ile oluştur. nil bir haritadan okumak güvenlidir, yazmak panik oluşturur. Sıralama için slices.SortFunc ve cmp.Or ile iki ölçütü birleştirebilirsin.

Çözümü göster
sayac/
├── go.mod
├── cmd/
│   └── sayac/
│       └── main.go
└── internal/
    └── counter/
        └── counter.go
internal/counter/counter.go
// Package counter, kelimelerin bir metinde kaç kez geçtiğini sayar.
package counter

import (
	"cmp"
	"maps"
	"slices"
)

// Counter kelime frekanslarını tutar. Sıfır değeri kullanıma hazırdır.
type Counter struct {
	counts map[string]int
}

// Add kelimenin sayacını bir artırır.
func (c *Counter) Add(word string) {
	if c.counts == nil {
		c.counts = make(map[string]int)
	}
	c.counts[word]++
}

// Count kelimenin kaç kez eklendiğini döndürür.
func (c *Counter) Count(word string) int {
	return c.counts[word]
}

// Top en sık geçen n kelimeyi döndürür; eşitlikte alfabetik sıra kullanılır.
func (c *Counter) Top(n int) []string {
	words := slices.Collect(maps.Keys(c.counts))
	slices.SortFunc(words, func(a, b string) int {
		return cmp.Or(cmp.Compare(c.counts[b], c.counts[a]), cmp.Compare(a, b))
	})
	return words[:min(n, len(words))]
}
cmd/sayac/main.go
package main

import (
	"fmt"
	"strings"

	"example.com/sayac/internal/counter"
)

func main() {
	text := "bir berber bir berbere gel beraber bir berber dükkânı açalım demiş"

	var c counter.Counter // sıfır değeri doğrudan kullanılabilir
	for _, w := range strings.Fields(text) {
		c.Add(w)
	}

	fmt.Println("bir:", c.Count("bir"))
	fmt.Println("berber:", c.Count("berber"))
	fmt.Println("en sık 3:", c.Top(3))
}
go vet ./...
go run ./cmd/sayac
go doc ./internal/counter
Çıktı
bir: 3
berber: 2
en sık 3: [bir berber açalım]
package counter // import "example.com/sayac/internal/counter"

Package counter, kelimelerin bir metinde kaç kez geçtiğini sayar.

type Counter struct{ ... }

counts alanı küçük harfle başladığı için paket dışından okunamaz ya da değiştirilemez; bütün erişim metotlardan geçer. Add metodu haritayı ilk kullanımda oluşturduğu için var c counter.Counter yazmak yeterli. Standart kütüphanedeki strings.Builder ve sync.Mutex de sıfır değeri kullanılabilir tiplerdir. Top, önce frekansa göre azalan, eşitlikte kelimeye göre artan sırada sıralıyor; cmp.Or ilk sıfır olmayan karşılaştırma sonucunu döndürür. Paket internal altında olduğu için yalnızca example.com/sayac modülündeki kod onu import edebilir.

Alıştırma·Aynı programda v1 ve v2
Zor

Bir çalışma alanında üç modül oluştur:

  • example.com/selam (v1): Hello(name string) string fonksiyonu Türkçe selam döndürsün.
  • example.com/selam/v2: Hello(name, lang string) (string, error) fonksiyonu tr, en ve de dillerini desteklesin, bilinmeyen dilde hata döndürsün. İmza v1 ile uyumsuz olduğu için bu bir major sürüm.
  • example.com/app: iki sürümü aynı programda kullansın.

v2 modülünü selam/v2 alt dizinine koy ve üç modülü go work ile birbirine bağla.

İpucu

selam/v2 dizininin kendi go.mod dosyası olmalı ve module satırı /v2 ile bitmeli. İki paketin adı da selam olacağı için app içinde birine takma ad ver. Çalışma alanını go work init ./app ./selam ./selam/v2 ile kurabilirsin.

Çözümü göster
surum/
├── go.work
├── app/
│   ├── go.mod          module example.com/app
│   └── main.go
└── selam/
    ├── go.mod          module example.com/selam
    ├── selam.go
    └── v2/
        ├── go.mod      module example.com/selam/v2
        └── selam.go
selam/selam.go
// Package selam, selamlama metinleri üretir.
package selam

// Hello Türkçe bir selam döndürür.
func Hello(name string) string {
	return "Merhaba, " + name + "!"
}
selam/v2/selam.go
// Package selam, birden çok dilde selamlama metinleri üretir.
package selam

import "fmt"

var greetings = map[string]string{
	"tr": "Merhaba",
	"en": "Hello",
	"de": "Hallo",
}

// Hello verilen dilde bir selam döndürür. v1'deki imzayla uyumlu değildir.
func Hello(name, lang string) (string, error) {
	g, ok := greetings[lang]
	if !ok {
		return "", fmt.Errorf("desteklenmeyen dil: %q", lang)
	}
	return g + ", " + name + "!", nil
}
app/main.go
package main

import (
	"fmt"

	"example.com/selam"
	selamv2 "example.com/selam/v2"
)

func main() {
	fmt.Println(selam.Hello("Deniz"))

	for _, lang := range []string{"en", "de", "fr"} {
		msg, err := selamv2.Hello("Deniz", lang)
		if err != nil {
			fmt.Println("hata:", err)
			continue
		}
		fmt.Println(msg)
	}
}
go work init ./app ./selam ./selam/v2
cat go.work
go run ./app
Çıktı
go 1.26.2

use (
	./app
	./selam
	./selam/v2
)
Merhaba, Deniz!
Hello, Deniz!
Hallo, Deniz!
hata: desteklenmeyen dil: "fr"

selam/v2 dizini kendi go.mod dosyasına sahip olduğu için üst dizindeki example.com/selam modülüne dahil değil; ayrı bir modül. İki modülün import yolları farklı olduğundan (example.com/selam ve example.com/selam/v2), Go ikisini aynı derlemede sorunsuzca kullanıyor. Paket adları aynı olduğu için v2'ye selamv2 takma adını verdik. Gerçek bir projede bu modüller yayımlandığında go.work dosyasına gerek kalmaz; app iki sürümü de go get ile ekler.

Kısa sınav

Kısa sınav

city paketinde tanımlanan adlardan hangisine main paketinden city.Ad biçiminde erişilebilir?

Bir pakette var x = y * 2 ve ondan sonra var y = 5 tanımlı. init fonksiyonu fmt.Println(x) çalıştırırsa ne yazdırılır?

example.com/shop/internal/payment paketini aşağıdakilerden hangisi import edebilir?

Modülün A v1.2.0 istiyor, bağımlılıklarından B ise A v1.4.0 istiyor. A modülünün yayımlanmış en yeni sürümü v1.7.0. Derlemede hangi sürüm kullanılır?

go.mod dosyası module github.com/ali/para/v2 diyen bir modülün kök paketini nasıl import edersin ve kodda hangi adla kullanırsın?

go.sum dosyasının asıl görevi nedir?

Özet

  • Paket aynı dizindeki dosyalardan oluşan derleme birimidir; modül kökünde go.mod bulunan ve birlikte sürümlenen paketler topluluğudur. Import yolu, modül yolu ile dizin yolunun birleşimidir.
  • Büyük harfle başlayan adlar dışa aktarılır, diğerleri paketin içinde kalır. Görünürlüğün sınırı dosya değil pakettir. Dışa aktarılmayan alanlar, paketin kendi kurallarını korumasını sağlar.
  • Paket düzeyi değişkenler bağımlılık sırasıyla başlatılır, ardından init fonksiyonları çalışır. Bir paket, import ettiği paketlerden sonra başlatılır; kardeş paketlerin sırasına güvenme ve init içine ağır iş koyma.
  • go.mod modül yolunu, en düşük Go sürümünü ve bağımlılıkların en düşük sürümlerini tutar. go.sum indirilen içeriğin bütünlüğünü doğrular. İkisi de commit edilir.
  • Minimal sürüm seçimi, istenen sürümlerin en büyüğünü seçer; derlemen kendiliğinden değişmez. Yükseltmeyi go get paket@sürüm ile sen yaparsın, go mod tidy ile dosyaları kodla uyumlu tutarsın.
  • v2 ve sonrası için modül yolu /v2 gibi bir sonek alır; farklı major sürümler aynı programda birlikte kullanılabilir.
  • internal dizinindeki paketleri yalnızca üst dizinin altındaki kod import edebilir. Uygulamalarda cmd/ ve internal/ düzeni yaygındır.
  • Yayımlanmamış modülleri yerelde birlikte geliştirmek için replace yerine go work kullan; belgeleri go doc ile terminalde, pkg.go.dev üzerinde de tarayıcıda okursun.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.