Go'da paket, aynı dizinde bulunan ve aynı package satırıyla başlayan kaynak dosyalarının topluluğudur. Bir paketi başka bir yerden kullanmak için onu import yoluyla içe aktarırsınız; import yolu modül yolu ile paketin modül içindeki dizininin birleşimidir. Paketin dışından yalnızca adı büyük harfle başlayan tanımlayıcılara erişilebilir; internal adlı bir dizinin altındaki paketleri ise yalnızca o dizinin üst ağacındaki kod içe aktarabilir.

Bu yazıda Go dil spesifikasyonunun paket ve başlatma bölümlerini, Effective Go'nun adlandırma önerilerini ve go.dev'deki "Organizing a Go module" rehberini birlikte kullanarak örnek bir proje kuruyoruz. Dışa aktarma kuralını, init fonksiyonlarının ve paket düzeyindeki değişkenlerin hangi sırayla çalıştığını, import biçimlerini ve derleyicinin bu konularda verdiği gerçek hata mesajlarını Go 1.27.3 ile çalıştırarak gösteriyoruz. Modülün kendisi, yani go.mod dosyası ve bağımlılıklar Go modül yönetimi yazısının konusu; dile genel bakış için Go öğrenme rehberi sayfasına dönebilirsiniz.

Paket ve kaynak dosyası

Spesifikasyona göre her kaynak dosyası bir paket satırıyla başlar, ardından isteğe bağlı import bildirimleri, sonra da fonksiyon, tip, değişken ve sabit bildirimleri gelir. Aynı paket adını taşıyan dosyalar birlikte paketin gerçeklemesini oluşturur ve bir dosyada tanımlanan her şey aynı paketin diğer dosyalarından doğrudan görülür; aynı paketteki dosyalar arasında import yazmazsınız. Go araçları bir dizindeki tüm .go dosyalarının aynı pakete ait olmasını bekler (test dosyalarındaki _test ekli paket istisnadır). Aynı dizine iki farklı paket adı koyarsanız go build şöyle durur (yol kısaltıldı):

text
found packages arac (a.go) and yardim (b.go) in .../iki/arac

Paket adı ile dizin adı teknik olarak farklı olabilir; içe aktaran dosya, import yolundaki son öğeyi değil paket satırındaki adı kullanır. Ancak Effective Go'daki gelenek, paket adının kaynak dizininin son adıyla aynı olmasıdır: src/encoding/base64 dizinindeki paket base64 adını taşır, encoding_base64 ya da encodingBase64 değil. Paket adları küçük harfli, tek kelimelik ve kısa olmalı, alt çizgi veya karışık büyük harf içermemelidir. Kullanıcılar adı her çağrıda yazacağı için kısalık önemlidir.

Paket adı içerideki adlandırmayı da etkiler. Effective Go'nun verdiği örnekte bufio paketindeki tamponlu okuyucunun adı BufReader değil Reader'dır; kullanıcı onu zaten bufio.Reader olarak görür ve io.Reader ile karışmaz. Benzer biçimde tek tip dışa aktaran bir ring paketinin kurucu fonksiyonu NewRing yerine New olarak adlandırılır ve ring.New diye okunur.

Örnek proje düzeni

Örnek proje düzeni

go.dev'deki "Organizing a Go module" rehberi küçük bir paketten çok komutlu sunucu projesine kadar birkaç düzen önerir. Hem içe aktarılabilir paket hem çalıştırılabilir komut içeren bir proje için önerilen yapıya uygun küçük bir mağaza modülü kurduk:

text
magaza/
  go.mod                 module example.com/magaza
  internal/
    fiyat/
      fiyat.go           package fiyat
  sepet/
    sepet.go             package sepet
  cmd/
    kasa/
      main.go            package main

Bu düzende sepet paketi modülü kullanan başkalarına açıktır ve example.com/magaza/sepet yoluyla içe aktarılır. KDV hesabını yapan fiyat paketi internal altında durduğu için yalnızca bu modülün içinden kullanılabilir. Çalıştırılabilir program cmd/kasa dizinindedir. Rehbere göre komutları cmd dizininde toplamak zorunlu değildir, ama hem paket hem komut içeren depolarda düzeni belirgin kılar. Programı kullananlar onu go install example.com/magaza/cmd/kasa@latest biçiminde kurabilir.

internal/fiyat/fiyat.go dosyası:

go
// Package fiyat, KDV hesaplarını yapar. Yalnızca bu modülün içinden kullanılabilir.
package fiyat

import "fmt"

// KDVOrani yüzde olarak tutulur.
const KDVOrani = 20

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

// KDVli, kuruş cinsinden net tutara KDV ekler.
func KDVli(kurus int) int {
	return kurus + kurus*KDVOrani/100
}

sepet/sepet.go dosyası, fiyat paketini içe aktarır ve dışa açık bir Sepet tipi sunar. Alanlardan Sahip büyük harfle başladığı için dışarıdan okunabilir, kalemler ise paket içinde kalır:

go
// Package sepet, alışveriş sepetini temsil eder.
package sepet

import (
	"fmt"

	"example.com/magaza/internal/fiyat"
)

var varsayilanAd = adUret()

func adUret() string {
	fmt.Println("değişken: varsayilanAd")
	return "misafir"
}

func init() {
	fmt.Println("init: sepet (1)")
}

func init() {
	fmt.Println("init: sepet (2)")
}

// Sepet, kalemlerin kuruş cinsinden net tutarlarını tutar.
type Sepet struct {
	Sahip    string
	kalemler []int
}

// Yeni, boş bir sepet döndürür.
func Yeni() *Sepet {
	return &Sepet{Sahip: varsayilanAd}
}

// Ekle, sepete bir kalem ekler.
func (s *Sepet) Ekle(kurus int) {
	s.kalemler = append(s.kalemler, kurus)
}

// Toplam, KDV dahil toplamı kuruş olarak döndürür.
func (s *Sepet) Toplam() int {
	t := 0
	for _, k := range s.kalemler {
		t += fiyat.KDVli(k)
	}
	return t
}

cmd/kasa/main.go ise programın giriş noktasıdır:

go
package main

import (
	"fmt"

	"example.com/magaza/sepet"
)

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

func main() {
	s := sepet.Yeni()
	s.Ekle(10000)
	s.Ekle(2550)
	fmt.Println(s.Sahip, s.Toplam())
}
bash
$ go run ./cmd/kasa
init: fiyat
değişken: varsayilanAd
init: sepet (1)
init: sepet (2)
init: main
misafir 15060
$ go list ./...
example.com/magaza/cmd/kasa
example.com/magaza/internal/fiyat
example.com/magaza/sepet

Toplam, 10000 ve 2550 kuruşa yüzde 20 KDV eklenerek bulunan 12000 + 3060 = 15060 kuruştur. Çıktının ilk beş satırı ise paketlerin hangi sırayla başlatıldığını gösteriyor; bunu birazdan ayrıntılı inceleyeceğiz. go list ./... modüldeki tüm paketleri import yollarıyla listeler; go test ./... ya da go vet ./... gibi komutlara verdiğiniz ./... deseninin neyi kapsadığını görmenin en kolay yolu budur.

Dışa aktarma: büyük harf kuralı

Go'da public veya private anahtar sözcüğü yoktur. Spesifikasyona göre bir tanımlayıcı iki koşul birlikte sağlanırsa dışa aktarılmış sayılır: adının ilk karakteri Unicode büyük harf kategorisinde (Lu) olmalı ve tanımlayıcı paket bloğunda bildirilmiş ya da bir alan veya metot adı olmalıdır. Fonksiyon içinde tanımlanan bir değişken büyük harfle başlasa bile dışa aktarılmaz. Kural yalnızca ilk harfin Unicode kategorisine baktığı için Ürün gibi Türkçe büyük harfle başlayan bir ad da dışa aktarılır; yine de import eden herkesin rahatça yazabilmesi için ASCII adlar daha pratiktir.

Dışa aktarılmamış bir alana ya da paket değişkenine başka paketten erişmeye çalıştığımızda derleyici iki farklı mesaj verir:

go
package main

import (
	"fmt"

	"example.com/magaza/sepet"
)

func main() {
	s := sepet.Yeni()
	fmt.Println(s.kalemler)
	fmt.Println(sepet.varsayilanAd)
}
text
# example.com/magaza/cmd/hata1
cmd/hata1/main.go:11:16: s.kalemler undefined (cannot refer to unexported field kalemler)
cmd/hata1/main.go:12:20: name varsayilanAd not exported by package sepet

Bu kural kapsüllemenin Go'daki temel aracıdır ve paket sınırında çalışır. Aynı paketteki tüm dosyalar birbirinin küçük harfli adlarını görür; bu yüzden bir tipin iç durumunu başka koddan korumak istiyorsanız onu ayrı bir pakete koymanız gerekir. Dışa aktarılan her ad için bir belge yorumu yazmanız da beklenir; go doc bu yorumları gösterir (bkz. Go araçları: gofmt, vet, doc).

internal dizini: modül içine kapalı paketler

Bir import yolunda internal öğesi geçiyorsa o paketi yalnızca internal dizininin üst dizinine kök olan ağaçtaki kod içe aktarabilir. Layout rehberi destekleyici paketleri başlangıçta internal altına koymayı önerir: dışarıdan kimse bu paketlere bağımlı olamayacağı için API'yi dilediğiniz gibi değiştirip kodu taşıyabilirsiniz. Bunu sınamak için example.com/baska adlı ayrı bir modül kurduk, mağaza modülünü replace ile yerel dizine bağladık ve internal/fiyat paketini içe aktarmayı denedik:

go
package main

import (
	"fmt"

	"example.com/magaza/internal/fiyat"
)

func main() {
	fmt.Println(fiyat.KDVli(100))
}
text
package example.com/baska
	main.go:6:2: use of internal package example.com/magaza/internal/fiyat not allowed

Aynı modül sepet paketini sorunsuz kullanabildi ve 100 kuruşluk kalem için 120 yazdırdı; sepet de fiyat paketini içeriden kullandığı için başlatma satırları yine göründü. Yani internal paketin kodu dolaylı olarak çalışabilir, yalnızca modül dışından doğrudan içe aktarılamaz.

Başlatma sırası: paket değişkenleri ve init

Başlatma sırası: paket değişkenleri ve init: İçe aktarılan paket, içe aktarandan önce başlatılır, Her paket yalnızca bir kez başlatılır, Önce paket değişkenleri, sonra init...

Spesifikasyonun "Program initialization" bölümü sırayı kesin biçimde tanımlar. Bir paketin içe aktardığı paketler, paketin kendisinden önce başlatılır; birden çok paket aynı paketi içe aktarsa bile o paket yalnızca bir kez başlatılır. Bir paketin içinde önce paket düzeyindeki tüm değişkenler değer alır, sonra init fonksiyonları kaynakta göründükleri sırayla çağrılır. Bütün bunlar tek bir goroutine içinde, paket paket ve sırayla yapılır; hepsi bittikten sonra main.main çağrılır.

Örnek çıktımızda bu sıranın her adımı görünür: main paketi sepet'i, sepet de fiyat'ı içe aktardığı için önce fiyat başlatıldı. Ardından sepet içindeki varsayilanAd değişkeni adUret() çağrısıyla değer aldı, sonra aynı dosyadaki iki init yazıldıkları sırayla çalıştı ve en son main paketinin init fonksiyonu geldi.

Paket içindeki değişkenler salt bildirim sırasıyla değil bağımlılıklarına göre başlatılır. Spesifikasyonun kuralı şöyledir: her adımda, bildirim sırasında en önde olan ve henüz başlatılmamış değişkenlere bağımlılığı bulunmayan değişken seçilir. Belgedeki örnekte a = c + b, b = f(), c = f(), d = 3 bildirimleri için başlatma sırası d, b, c, a olur. Bir pakete ait dosyalar derleyiciye verildiği sırayla işlenir ve belge, derleme sistemlerini dosyaları ad sırasına göre vermeye teşvik eder; yine de farklı dosyalardaki değişkenlerin gizli yan etkilerle birbirine bağlanmasına güvenmeyin.

KuralKaynağı
İçe aktarılan paket, içe aktarandan önce başlatılırSpesifikasyon: Program initialization
Her paket yalnızca bir kez başlatılırSpesifikasyon: Program initialization
Önce paket değişkenleri, sonra init fonksiyonlarıSpesifikasyon: Package initialization
Bir dosyada birden çok init olabilir, kaynak sırasıyla çalışırSpesifikasyon ve Effective Go
init çağrılamaz ve ona başvurulamazSpesifikasyon: Package initialization
Başlatma tek goroutine içinde ve sırayla yapılırSpesifikasyon: Program initialization

Effective Go, init için iki tipik kullanım sayar: bildirimle ifade edilemeyen başlatmalar ve gerçek çalışma başlamadan önce program durumunun doğrulanması. init fonksiyonunun parametresi ve dönüş değeri olamaz; programın başka bir yerinden çağrılamaz. Yan etkisi kodu okuyana görünmediği için şaşırtıcı olabilir; mümkünse açık bir kurucu fonksiyon (Yeni gibi) tercih edin ve init içinde ağ bağlantısı ya da dosya açma gibi başarısız olabilecek işler yapmaktan kaçının.

Import biçimleri

Import bildirimi, içe aktarılan paketin dışa aktardığı adlara erişimi açar. Spesifikasyon üç biçim tanımlar: varsayılan ad, açık bir takma ad ve nokta. Bunlara yalnızca yan etki için yapılan, boş tanımlayıcılı import eklenir. Hepsini tek programda gösterelim:

go
package main

import (
	"fmt"
	str "strings" // takma ad: paket str adıyla kullanılır

	_ "embed" // boş tanımlayıcı: yalnızca yan etkisi için
	. "math"  // nokta: dışa aktarılan adlar niteleyicisiz kullanılır
)

func main() {
	fmt.Println(str.ToUpper("go")) // GO
	fmt.Println(Sqrt(16), Pi > 3)  // 4 true
}
BildirimKullanımNe zaman
import "strings"strings.ToUpperVarsayılan; neredeyse her zaman
import str "strings"str.ToUpperİki paketin adı çakıştığında
import . "math"SqrtEffective Go kaçınmayı önerir
import _ "embed"Ad yokYalnızca paketin başlatma yan etkisi için

Boş tanımlayıcılı import en çok, init fonksiyonunda kendini bir yere kaydeden paketlerde görülür. Effective Go'nun örneği net/http/pprof paketidir: içe aktarıldığında hata ayıklama için HTTP işleyicilerini kaydeder ve çoğu program paketin başka bir adını hiç kullanmaz, bu yüzden import _ "net/http/pprof" yazılır. Bu örnekteki _ "embed" satırı ise yalnızca sözdizimini göstermek için eklendi; embed paketinin gerçek kullanımı Go embed ile dosya gömme yazısında anlatılıyor.

Kullanılmayan import ve import döngüsü

Spesifikasyon, bir paketi içe aktarıp dışa aktardığı hiçbir ada başvurmamayı yasaklar. Effective Go bunun gerekçesini de açıklar: kullanılmayan importlar programı şişirir ve derlemeyi yavaşlatır. Bu yüzden Go'da kullanılmayan bir import uyarı değil derleme hatasıdır:

go
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("merhaba")
}
text
# command-line-arguments
./main.go:5:2: "os" imported and not used

Bir paket kendisini doğrudan ya da dolaylı yoldan içe aktaramaz. a paketi b'yi, b de a'yı içe aktarırsa go build zinciri gösterip durur:

text
package example.com/dongu/a
	imports example.com/dongu/b from a.go
	imports example.com/dongu/a from b.go: import cycle not allowed

Döngü genellikle sorumlulukların yanlış bölündüğünü gösterir. Yaygın çözümler, iki paketin ortak ihtiyaç duyduğu tipleri üçüncü bir pakete taşımak ya da bağımlılığın yönünü tersine çevirmek için kullanan tarafta küçük bir arayüz tanımlamaktır (arayüzler için Go interface rehberi). Spesifikasyon da importların bu yapısı sayesinde başlatma bağımlılıklarında döngü oluşamayacağını not eder.

package main ve birden çok komut

Spesifikasyona göre çalıştırılabilir bir program, içe aktarılmayan tek bir main paketinin, içe aktardığı tüm paketlerle bağlanmasıyla oluşur. Bu paketin adı main olmalı ve parametresiz, dönüş değersiz bir main fonksiyonu bildirmelidir. main döndüğünde program biter ve diğer goroutine'lerin bitmesini beklemez (ayrıntısı Go goroutine rehberi yazısında). Bir depoda birden çok program varsa her biri kendi dizininde package main bildirir; layout rehberi ortak kodu üst düzey bir internal dizininde toplamayı önerir. Sunucu projelerinde dışa aktarılacak paket genellikle olmadığı için iş mantığının tamamını internal altında tutmak, komutları da cmd altında toplamak önerilir.

Testler için bir istisna vardır: bir dizinde paketadi_test adlı ayrı bir test paketi bulunabilir. Bu paket test edilen paketi dışarıdan, yalnızca dışa aktarılan adlarıyla kullanır. Bu "kara kutu" yaklaşımı Go test yazma rehberi yazısında örnekleniyor.

Sık yapılan hatalar

Sık yapılan hatalar: Aynı dizine iki paket koymak, Paket adını dizin adından farklı seçmek, Her şeyi tek bir util ya da common paketine doldurmak, Gereksiz yere her şeyi dışa...
  • Aynı dizine iki paket koymak: Bir dizin tek paket demektir; found packages hatası alırsanız dosyaları ayrı dizinlere taşıyın.
  • Paket adını dizin adından farklı seçmek: Derlenir ama okuyanı yanıltır. Gelenek, paket adının dizinin son adıyla aynı olmasıdır.
  • Her şeyi tek bir util ya da common paketine doldurmak: Paket adı kullanım yerinde görünür; util.Format neyi biçimlendirdiğini söylemez. Paketleri sundukları işe göre adlandırın.
  • Gereksiz yere her şeyi dışa aktarmak: Büyük harfle başlayan her ad, kullanıcılarınıza verdiğiniz bir sözdür. Emin değilseniz küçük harfle başlayın ya da paketi internal altına koyun.
  • init fonksiyonunda ağır iş yapmak: Hata döndüremez, test edilmesi zordur ve paketi içe aktaran her program bu maliyeti öder.
  • Nokta importunu alışkanlık hâline getirmek: Adların hangi paketten geldiği belirsizleşir; Effective Go bu biçimden genel olarak kaçınmayı önerir.

Sık Sorulan Sorular

Paket ile modül arasındaki fark nedir?

Paket derleme birimidir: tek dizindeki dosyalar. Modül ise sürümleme ve dağıtım birimidir: go.mod dosyasının altındaki paketlerin tamamı. Bir modülde çok sayıda paket olabilir; bir paket ise tek bir modüle aittir.

Aynı paketteki bir dosyadan diğerine import yazmam gerekir mi?

Hayır. Aynı dizindeki ve aynı paket adını taşıyan dosyalar tek paket sayılır; birinde tanımlanan fonksiyon, tip ya da değişken diğerinde import yazmadan ve niteleyici kullanmadan görülür.

init fonksiyonları hangi sırayla çalışır?

Önce içe aktarılan paketler başlatılır. Bir paketin içinde önce tüm paket değişkenleri değer alır, sonra init fonksiyonları derleyiciye verilen dosya sırası ve dosya içindeki kaynak sırasıyla çağrılır. Sıraya dayanan kod yazmak yerine bağımlılığı açık fonksiyon çağrılarıyla kurmak daha güvenlidir.

internal dizinini ne zaman kullanmalıyım?

Modülünüzün dışına söz vermek istemediğiniz her yardımcı paket için. Layout rehberi paketleri mümkün olduğunca internal içinde tutmayı önerir; bir paket gerçekten başkalarına yararlı hâle geldiğinde onu dışarı taşıyabilir ya da ayrı bir modüle ayırabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Go öğrenme rehberi · Go modül yönetimi · Go test yazma · gofmt, go vet ve go doc · go build ve çapraz derleme · Go'da sık karşılaşılan hatalar