Go'da arayüz (interface), bir tipin ne yapabildiğini metot imzalarıyla tanımlayan bir tiptir. Bir tip, arayüzdeki bütün metotlara sahipse o arayüzü kendiliğinden uygular; Java veya C# gibi dillerdeki implements bildirimi yoktur. Arayüz tipindeki bir değişken, bu metotlara sahip herhangi bir değeri tutabilir ve metot çağrıları o anda içindeki somut değere yönlendirilir. Hiç metodu olmayan arayüz interface{}, kısa adıyla any, her değeri tutar.

Bu yazıda örtük uygulamayı, küçük arayüz tasarımını, fmt.Stringer ve io.Writer gibi standart arayüzleri, arayüz değerinin (tip, değer) yapısını, ünlü "nil olmayan nil error" tuzağını, arayüz gömmeyi ve derleme anında uygunluk kontrolünü Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Bir arayüzün içindeki somut tipi geri almak için kullanılan x.(T) ve type switch type assertion yazısında ele alınıyor. Serinin tamamı için Go öğrenme rehberi sayfasına bakabilirsiniz.

Arayüz tanımlamak ve örtük uygulama

Arayüz tanımlamak ve örtük uygulama

Aşağıdaki Sekil arayüzü iki metot istiyor. Dikdortgen ve Daire tipleri bu metotları tanımlıyor ama hiçbir yerde Sekil'in adını anmıyor. Yine de ikisi de bir []Sekil dilimine konabiliyor ve Ozet fonksiyonu hangi şekil olduğunu bilmeden çalışıyor.

go
package main

import (
	"fmt"
	"math"
)

type Sekil interface {
	Alan() float64
	Cevre() float64
}

type Dikdortgen struct{ En, Boy float64 }

func (d Dikdortgen) Alan() float64  { return d.En * d.Boy }
func (d Dikdortgen) Cevre() float64 { return 2 * (d.En + d.Boy) }

type Daire struct{ R float64 }

func (c Daire) Alan() float64  { return math.Pi * c.R * c.R }
func (c Daire) Cevre() float64 { return 2 * math.Pi * c.R }

func Ozet(s Sekil) string {
	return fmt.Sprintf("%T alan=%.2f çevre=%.2f", s, s.Alan(), s.Cevre())
}

func main() {
	sekiller := []Sekil{Dikdortgen{3, 4}, Daire{1}}
	for _, s := range sekiller {
		fmt.Println(Ozet(s))
	}
}

// Çıktı:
// main.Dikdortgen alan=12.00 çevre=14.00
// main.Daire alan=3.14 çevre=6.28

Spesifikasyon bunu "tip kümesi" (type set) diliyle anlatır: bir arayüz bir tip kümesi tanımlar ve bu kümedeki her tip arayüzü uygular. Yalnızca metotlardan oluşan arayüzler için küme, o metotların hepsine sahip tiplerden oluşur. Go FAQ, implements bildiriminin olmamasının bilinçli bir tasarım olduğunu söyler: arayüzler mevcut kodu değiştirmeden sonradan tanımlanabilir, bir tip aynı anda birçok arayüzü uygulayabilir ve yönetilmesi gereken bir tip hiyerarşisi oluşmaz. Örneğin test için bir arayüz tanımlayıp mevcut bir tipi ona uydurmak, o tipin koduna dokunmadan mümkündür.

Küçük arayüzler ve isimlendirme

Effective Go'ya göre Go kodunda bir veya iki metotlu arayüzler yaygındır ve tek metotlu arayüzler genellikle metot adına -er eki getirilerek adlandırılır: Reader, Writer, Formatter. Aynı belge, Read, Write, Close, String gibi kanonik adları yalnızca aynı imza ve anlamla kullanmanızı önerir; metninize dönüştüren metodu ToString değil String diye adlandırın.

Standart kütüphanenin en çok kullanılan arayüzlerinden ikisi io.Writer (Write([]byte) (int, error)) ve fmt.Stringer'dır (String() string). Aşağıda kendi yazdığımız SatirSayaci tipi Write metodu sayesinde io.Writer oluyor ve fmt.Fprintf doğrudan ona yazabiliyor. Kullanici ise String metodu sayesinde fmt tarafından kendi biçimiyle yazdırılıyor:

go
package main

import (
	"fmt"
	"os"
)

// SatirSayaci, io.Writer arayüzünü örtük olarak uygular.
type SatirSayaci struct {
	bayt, satir int
}

func (s *SatirSayaci) Write(p []byte) (int, error) {
	s.bayt += len(p)
	for _, b := range p {
		if b == '\n' {
			s.satir++
		}
	}
	return len(p), nil
}

type Kullanici struct {
	Ad  string
	Yas int
}

func (k Kullanici) String() string {
	return fmt.Sprintf("%s (%d)", k.Ad, k.Yas)
}

func main() {
	var sayac SatirSayaci
	fmt.Fprintf(&sayac, "merhaba %s\n", "dünya")
	fmt.Fprintln(&sayac, "ikinci satır")
	fmt.Println(sayac.bayt, sayac.satir)

	k := Kullanici{"Ayşe", 31}
	fmt.Println(k)
	fmt.Fprintf(os.Stdout, "%v | %s | %+v\n", k, k, struct{ K Kullanici }{k})
}

// Çıktı:
// 29 2
// Ayşe (31)
// Ayşe (31) | Ayşe (31) | {K:Ayşe (31)}

Bayt sayısının 29 çıkması, "ü" ve "ı" harflerinin UTF-8'de ikişer bayt olmasındandır (bkz. string, rune ve byte yazısı). Write işaretçi alıcılı olduğu için fmt.Fprintf'e &sayac geçirdik; bunun nedeni metot kümeleri kuralıdır. FAQ, Unix borularına benzeyen bu yapının, yani fmt.Fprintf'in yalnızca dosyaya değil her türlü çıktıya yazabilmesinin tek bir tek-metotlu arayüze (io.Writer) dayandığını özellikle örnek verir. Biçimlendirme fiillerinin ayrıntısı fmt Printf yazısında anlatılıyor.

Arayüz değeri: içeride bir tip ve bir değer var

Bir arayüz değişkeni, şematik olarak iki parça taşır: içindeki somut değerin dinamik tipi ve dinamik değeri. FAQ bunu (T, V) çifti olarak anlatır: 3 sayısını bir arayüze koyarsanız içerik (T=int, V=3) olur. Arayüz değeri ancak her ikisi de boşsa nil'dir. Bu kural, Go'ya yeni başlayanların en sık takıldığı hatalardan birine yol açar: nil bir işaretçiyi arayüze koyduğunuzda tip kısmı dolu olduğu için arayüz artık nil değildir.

go
package main

import "fmt"

type Hata struct{ Kod int }

func (h *Hata) Error() string { return fmt.Sprintf("hata %d", h.Kod) }

func kontrol(bozuk bool) error {
	var h *Hata // nil işaretçi
	if bozuk {
		h = &Hata{Kod: 42}
	}
	return h // DİKKAT: nil *Hata, nil olmayan bir error olur
}

func kontrolDogru(bozuk bool) error {
	if bozuk {
		return &Hata{Kod: 42}
	}
	return nil
}

func main() {
	var e error
	fmt.Printf("(%v, %T) nil mi: %v\n", e, e, e == nil)

	err := kontrol(false)
	fmt.Printf("(%v, %T) nil mi: %v\n", err, err, err == nil)

	err = kontrolDogru(false)
	fmt.Println(err == nil)
}

// Çıktı:
// (<nil>, <nil>) nil mi: true
// (<nil>, *main.Hata) nil mi: false
// true

İkinci satır tuzağı gösteriyor: kontrol(false) hiçbir hata oluşmadığı halde err != nil sonucunu veriyor, çünkü dönen error değerinin içinde (T=*main.Hata, V=nil) var. FAQ'ın önerisi iki adımlıdır: hata döndüren fonksiyonların imzasında somut tip (*Hata) yerine her zaman error kullanın ve başarılı durumda açıkça return nil yazın. os.Open da bu yüzden, başarısızlıkta hep *os.PathError döndürmesine rağmen imzasında error kullanır. Hata yönetiminin geri kalanı Go error yazısında anlatılıyor.

Boş arayüz: any

Boş arayüz: any

Hiç metot istemeyen interface{} her tipi kabul eder; Go 1.18'den beri bunun için önceden tanımlı any takma adı vardır. Spesifikasyona göre any yeni bir tip değil, boş arayüzün takma adıdır. fmt.Println(a ...any) gibi imzalar bu yüzden her değeri alabilir. Ama any içindeki değerle bir şey yapmak için onu önce bir tip iddiası veya type switch ile somut tipe çevirmeniz gerekir; bu da tip güvenliğini çalışma zamanına taşır. Aynı kodu birçok tip için yazmanız gerekiyorsa çoğu zaman generics daha iyi bir araçtır.

FAQ'ta yer alan bir başka ayrıntı: []int bir []any'ye doğrudan dönüştürülemez, çünkü iki tipin bellekteki temsili farklıdır. Elemanları tek tek kopyalamanız gerekir. Aynı şekilde, alttaki tipi aynı olsa bile []T1 doğrudan []T2'ye çevrilemez.

Arayüz değerlerini karşılaştırmak

Arayüz değerleri == ile karşılaştırılabilir: dinamik tipleri aynı ve dinamik değerleri eşitse ya da ikisi de nil ise eşittirler. Ancak dinamik tipler aynı ve bu tip karşılaştırılamayan bir tipse (dilim, map, fonksiyon) karşılaştırma derleme anında değil, çalışma zamanında paniğe yol açar:

go
package main

import "fmt"

func main() {
	var a, b any = 1, 1
	fmt.Println(a == b)
	a, b = "x", 1
	fmt.Println(a == b)
	a, b = []int{1}, []int{1}
	fmt.Println(a == b)
}

// Çıktı:
// true
// false
// panic: runtime error: comparing uncomparable type []int

Spesifikasyonun map tipleri bölümüne göre, anahtar tipi arayüz olan bir map'te dinamik anahtar değeri karşılaştırılamıyorsa (ör. dilim) yine çalışma zamanı paniği oluşur. Bu risk, generics kısıtları yazısında anlatılan comparable kısıtının neden bu kadar ayrıntılı tanımlandığını da açıklar.

Arayüz gömme

Bir arayüz, başka arayüzleri adlarıyla gömerek onların metotlarını kendi metot kümesine katabilir. Spesifikasyona göre gömülen arayüzlerde aynı adlı metotlar varsa imzaları aynı olmalıdır. Standart kütüphanedeki io.ReadWriter bu biçimde tanımlanır. Geniş bir arayüz değerini daha dar bir arayüz değişkenine atamak her zaman serbesttir:

go
package main

import "fmt"

type Okuyucu interface {
	Oku() string
}

type Yazici interface {
	Yaz(s string)
}

// Gömme: OkuyucuYazici, iki arayüzün yöntemlerinin birleşimidir.
type OkuyucuYazici interface {
	Okuyucu
	Yazici
}

type Bellek struct{ veri []string }

func (b *Bellek) Yaz(s string) { b.veri = append(b.veri, s) }
func (b *Bellek) Oku() string {
	if len(b.veri) == 0 {
		return ""
	}
	s := b.veri[0]
	b.veri = b.veri[1:]
	return s
}

func main() {
	var ow OkuyucuYazici = &Bellek{}
	ow.Yaz("bir")
	ow.Yaz("iki")

	var o Okuyucu = ow // geniş arayüzden dar arayüze atama
	fmt.Println(o.Oku(), ow.Oku(), o.Oku() == "")
}

// Çıktı:
// bir iki true

Tersi, yani dar bir arayüz değerinden geniş bir arayüze geçmek, derleyicinin kendiliğinden yapabileceği bir şey değildir; içerideki değerin ek metotlara sahip olup olmadığı ancak çalışma zamanında bir tip iddiasıyla sorulabilir. Bu "isteğe bağlı yetenek" deseni type assertion yazısında örnekleniyor.

Arayüz döndürmek ve uygulamayı gizlemek

Effective Go'nun "Generality" bölümü, bir tip yalnızca bir arayüzü uygulamak için varsa ve o arayüzün ötesinde dışa açık metodu olmayacaksa tipin kendisini dışa aktarmaya gerek olmadığını söyler. Bu durumda yapıcı fonksiyon somut tipi değil arayüz değerini döndürmelidir. Belgedeki örnekte hem crc32.NewIEEE hem adler32.New aynı hash.Hash32 arayüzünü döndürür; bu sayede bir programda CRC-32 algoritmasını Adler-32 ile değiştirmek yalnızca yapıcı çağrısını değiştirmeyi gerektirir, kodun geri kalanı etkilenmez. crypto/cipher paketindeki Block ve Stream arayüzleri de aynı fikirle blok şifreleme algoritmasını, onu akışa çeviren kipten ayırır.

Bu öneriyi, "hata döndürürken somut tip yerine error kullanın" kuralıyla birlikte düşünün. Somut tipi döndürmek çağırana bütün metotlara erişim verir; arayüz döndürmek ise uygulamayı ileride değiştirme özgürlüğünü korur. Karar, tipin arayüz dışında anlamlı bir davranışı olup olmadığına göre verilir. Yapıcı arayüz döndürse bile çağıran taraf gerekirse içteki somut tipi bir tip iddiasıyla alabilir; ama buna sık ihtiyaç duyuluyorsa arayüz büyük olasılıkla yanlış seçilmiştir.

Derleme anında uygunluk kontrolü

Örtük uygulamanın bir bedeli, bir metodun imzasını yanlış yazdığınızda tipin arayüzü sessizce uygulamamaya başlamasıdır. Kodun bir yerinde tip arayüze atanıyorsa derleyici bunu zaten yakalar. Böyle bir statik atama yoksa, Effective Go ve FAQ'ta önerilen boş tanımlayıcılı bildirimle kontrolü kendiniz ekleyebilirsiniz:

go
package main

type Kaydedici interface {
	Kaydet(veri string) error
}

type Dosya struct{}

func (d Dosya) Kaydet(veri []byte) error { return nil }

var _ Kaydedici = Dosya{}

func main() {}
text
# command-line-arguments
./main.go:11:19: cannot use Dosya{} (value of struct type Dosya) as Kaydedici value in variable declaration: Dosya does not implement Kaydedici (wrong type for method Kaydet)
		have Kaydet([]byte) error
		want Kaydet(string) error

Mesaj, olan (have) ve beklenen (want) imzaları alt alta gösterdiği için hatayı bulmak kolaydır. Metotlar işaretçi alıcılıysa kontrolü var _ Kaydedici = (*Dosya)(nil) biçiminde yazarsınız. Effective Go bu bildirimin her tip için değil, yalnızca kodda statik bir dönüşümün bulunmadığı nadir durumlarda kullanılmasını önerir.

Arayüzlerle ilgili kavram tablosu

KavramAnlamı
Örtük uygulamaMetotlar varsa tip arayüzü uygular, ayrıca bildirim gerekmez
Dinamik tip ve değerArayüz değişkeninin içinde o an duran somut tip ve değer
nil arayüzTip ve değer ikisi de boş; yalnızca bu durumda == nil doğrudur
anyinterface{} için takma ad, her tipi tutar
GömmeBir arayüzün başka arayüzlerin metotlarını içermesi
var _ I = T{}T tipinin I arayüzünü uyguladığını derleme anında doğrular

Sık yapılan hatalar

Sık yapılan hatalar: nil işaretçiyi error olarak döndürmek, Arayüze işaretçi kullanmak, Gereğinden büyük arayüzler, Metot imzası farkı
  • nil işaretçiyi error olarak döndürmek: var h *Hata; return h nil olmayan bir error üretir. Başarılı durumda return nil yazın.
  • Arayüze işaretçi kullanmak: FAQ'a göre arayüz işaretçisine (*io.Writer) neredeyse hiç ihtiyaç olmaz. fmt.Fprintf(&w, ...) gibi bir çağrı, w zaten bir io.Writer ise derlenmez.
  • Gereğinden büyük arayüzler: Kullanmadığınız metotları da isteyen bir arayüz, ona uyan tip sayısını azaltır ve test yazmayı zorlaştırır.
  • Metot imzası farkı: FAQ'taki Equaler örneğinde olduğu gibi, Equal(T) bool metodu Equal(Equaler) bool isteyen arayüzü uygulamaz; Go'da parametre tipleri birebir aynı olmalıdır.
  • Dilim tutan arayüzleri karşılaştırmak: == çalışma zamanında panik verebilir.
  • Her şeyi any ile yazmak: Tip denetimi kaybolur ve kod tip iddialarıyla dolar; davranış için arayüz, aynı kodu farklı tiplerle kullanmak için generics tercih edin.

Sık Sorulan Sorular

Bir tipin bir arayüzü uyguladığını nasıl garanti ederim?

Paket düzeyinde var _ Arayuz = Tip{} ya da işaretçi alıcılar için var _ Arayuz = (*Tip)(nil) yazın. Tip arayüzü uygulamazsa derleme hata verir. FAQ ayrıca, kullanıcıların arayüzü bilerek uygulamasını istiyorsanız arayüze açıklayıcı adlı bir işaretleyici metot eklemenin mümkün olduğunu, ama bunun arayüz fikrinin esnekliğini azalttığı için nadiren kullanıldığını belirtir.

Go'da kalıtım yok mu?

Yoktur. FAQ'a göre Go, tipler arası ilişkileri önceden bildirmek yerine bir tipin, metotlarının bir alt kümesini isteyen her arayüzü kendiliğinden uygulamasını tercih eder. Kod paylaşımı için struct gömme kullanılır; bu kalıtıma benzer ama aynı şey değildir.

Arayüz mü kullanmalıyım, generics mi?

Go Blog'daki "When To Use Generics" yazısının kuralı şudur: bir değerle yapacağınız tek şey onun bir metodunu çağırmaksa arayüz kullanın. Aynı kodu yalnızca tipleri farklı olacak şekilde tekrar tekrar yazdığınızı fark ettiğinizde tip parametrelerini düşünün.

Bir arayüzün işaretçisini kullanmalı mıyım?

FAQ'ın cevabı "neredeyse hiçbir zaman"dır. Somut bir tipin işaretçisi bir arayüzü uygulayabilir, ama tek bir istisna dışında bir arayüzün işaretçisi hiçbir arayüzü uygulamaz. İstisna boş arayüzdür: her değer, arayüz işaretçisi de dahil, any tipindeki bir değişkene atanabilir; FAQ'a göre bu da büyük olasılıkla bir hatadır ve kafa karıştırıcı sonuçlar doğurur. Arayüzleri her zaman değer olarak geçirin.

Arayüz değerinin içindeki tipi nasıl öğrenirim?

fmt.Printf("%T", v) dinamik tipi yazdırır. Kod içinde karar vermek için tip iddiası (v.(Tip)) veya type switch kullanılır; ayrıntılar type assertion ve type switch yazısında.

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 metotlar · Go type switch · Go hata yönetimi · Go generics · Go nedir