Duvardaki bir prizi düşün. Priz, kendisine takılan cihazın lamba mı, telefon şarj aleti mi, yoksa elektrik süpürgesi mi olduğunu bilmez ve umursamaz. Tek beklentisi fişin doğru biçimde olmasıdır. Bu sayede evdeki tesisat, henüz icat edilmemiş cihazlarla bile çalışır. Go'daki arayüzler (interface) de böyle bir sözleşmedir: Bir değerin ne olduğunu değil, ne yapabildiğini tarif eder.
Arayüzler Go'nun temel soyutlama aracıdır. fmt.Println her tipi yazdırabiliyorsa, io.Copy bir dosyadan ağ bağlantısına ya da bellekteki bir tampona aynı kodla veri aktarabiliyorsa, bunun arkasında arayüzler vardır. Go'yu Java veya C# gibi dillerden ayıran önemli bir fark da şudur: Bir tipin bir arayüzü uyguladığını hiçbir yerde bildirmezsin. Gerekli metotlara sahip olan her tip o arayüzü kendiliğinden sağlar ve derleyici bunu derleme anında denetler.
Bu derste arayüz tanımlamayı ve örtük uygulamayı, bir arayüz değerinin bellekte nasıl bir (tip, değer) çifti olarak durduğunu, any tipini, tip iddiası ve tip switch ile arayüzün içindeki somut değere ulaşmayı öğreneceksin. fmt.Stringer, io.Reader ve io.Writer gibi standart kütüphanenin en önemli küçük arayüzleriyle çalışacak, Go'da en çok kafa karıştıran hatalardan biri olan nil arayüz tuzağını çalışan bir örnekle göreceksin. Son olarak iyi bir arayüzün nasıl tasarlandığını konuşacağız.
Arayüz tanımlama ve örtük uygulama
Arayüz tanımlama
Bir arayüz, metot imzalarından oluşan bir kümedir. Geometrik şekillerle çalışan bir program yazdığını düşün. Programın geri kalanı için bir şeklin dikdörtgen mi daire mi olduğu önemli değil; alanını ve çevresini hesaplayabilmesi yeterli:
type Shape interface {
Area() float64 // alan
Perimeter() float64 // çevre
}Bu tanım "Area() float64 ve Perimeter() float64 metotlarına sahip her tip bir Shape'tir" der. Arayüz alan (field) içermez, metotların nasıl çalıştığını da söylemez; yalnızca hangi davranışların beklendiğini listeler.
Örtük uygulama
Şimdi iki somut tip yazalım ve bunları Shape bekleyen bir fonksiyona verelim:
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Rect struct {
Width, Height float64
}
func (r Rect) Area() float64 { return r.Width * r.Height }
func (r Rect) Perimeter() float64 { return 2 * (r.Width + r.Height) }
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func describe(s Shape) {
fmt.Printf("%T -> alan: %.2f, çevre: %.2f\n", s, s.Area(), s.Perimeter())
}
func main() {
shapes := []Shape{
Rect{Width: 3, Height: 4},
Circle{Radius: 1},
}
total := 0.0
for _, s := range shapes {
describe(s)
total += s.Area()
}
fmt.Printf("toplam alan: %.2f\n", total)
}main.Rect -> alan: 12.00, çevre: 14.00 main.Circle -> alan: 3.14, çevre: 6.28 toplam alan: 15.14
Kodda implements gibi bir anahtar kelime yok. Rect ve Circle, Shape'in istediği iki metoda sahip oldukları için Shape arayüzünü örtük olarak (implicitly) sağlarlar. describe fonksiyonu şekillerin somut tiplerini bilmeden onlarla çalışır; yarın bir Triangle tipi eklediğinde describe'a tek satır dokunman gerekmez.
Örtük uygulamanın pratikteki büyük faydası bağımlılıkları tersine çevirmesidir. Standart kütüphanedeki *os.File tipi senin paketindeki bir arayüzden habersizdir; ama sen Read metodunu isteyen bir arayüz tanımlarsan *os.File o arayüzü hemen sağlar. Arayüzü uygulayan paketin, arayüzü tanımlayan paketi içe aktarması (import) gerekmez.
Buna "ördek tiplemesi" (duck typing) benzetmesi yapılır: Ördek gibi yürüyor ve ördek gibi ötüyorsa ördektir. Farkı, Go'nun bu kontrolü çalışma anında değil derleme anında yapmasıdır. Eksik bir metot varsa program hiç derlenmez.
Metot kümeleri: değer alıcı mı, işaretçi alıcı mı?
Bir tipin hangi arayüzleri sağladığını, o tipin metot kümesi (method set) belirler. Kural şudur:
Yani metotların işaretçi alıcılıysa arayüzü yalnızca *T sağlar. Aşağıdaki program iki farklı nedenle derlenmez:
package main
import "fmt"
type Shape interface {
Area() float64
Perimeter() float64
}
type Rect struct{ Width, Height float64 }
func (r Rect) Area() float64 { return r.Width * r.Height }
// Perimeter metodu unutuldu!
type Square struct{ Side float64 }
func (s *Square) Area() float64 { return s.Side * s.Side }
func (s *Square) Perimeter() float64 { return 4 * s.Side }
func main() {
var a Shape = Rect{Width: 2, Height: 3}
var b Shape = Square{Side: 2}
fmt.Println(a, b)
}./main.go:22:16: cannot use Rect{…} (value of struct type Rect) as Shape value in variable declaration: Rect does not implement Shape (missing method Perimeter)
./main.go:23:16: cannot use Square{…} (value of struct type Square) as Shape value in variable declaration: Square does not implement Shape (method Area has pointer receiver)İlk hata açık: Rect'in Perimeter metodu yok. İkinci hatanın çözümü ise Square{Side: 2} yerine &Square{Side: 2} yazmaktır. Bu kuralın nedeni, arayüzün içine konan bir değerin kopyasının adresinin alınamamasıdır. Değer bir arayüze konduğunda Go onun bir kopyasını saklar; işaretçi alıcılı bir metot bu kopyayı değiştirseydi değişiklik hiçbir yere yansımazdı. Go bu sessiz hatayı derleme hatasına çevirir. Alıcı tipleri arasındaki farkı Struct'lar ve Metotlar dersinde, adres alınabilirliği İşaretçiler dersinde görmüştün.
Derleme zamanında garanti almak
Bir tipin belirli bir arayüzü sağlaması gerekiyor ama paketin içinde onu o arayüze atayan bir satır yoksa, eksik metot hatası ancak başka bir pakette, belki haftalar sonra ortaya çıkar. Bunu önlemek için Go'da yaygın bir deyim vardır:
// Rect, Shape arayüzünü sağlamak zorunda; sağlamazsa derleme bu satırda durur.
var _ Shape = Rect{}
// İşaretçi alıcılı tipler için: bellek ayırmadan, tipli bir nil işaretçiyle kontrol.
var _ Shape = (*Square)(nil)_ (boş tanımlayıcı) sayesinde bu değişkenler hiçbir yerde kullanılmaz ve çalışma zamanında hiçbir maliyetleri yoktur. Tek işlevleri derleyiciye "bu atama geçerli olmalı" dedirtmektir. (*Square)(nil) ifadesi, *Square tipinde bir nil işaretçidir; Square değeri oluşturmadan metot kümesini kontrol etmenin en ucuz yoludur.
Arayüz değerleri: (tip, değer) çifti
Bir arayüz tipindeki değişkeni iki gözlü bir kutu gibi düşünebilirsin. Bir gözde içindeki değerin dinamik tipi, diğer gözde dinamik değeri durur. Değişkenin statik tipi Shape olarak sabittir; ama içindeki dinamik tip ve değer her atamayla değişebilir.
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
type Square struct{ Side float64 }
func (s *Square) Area() float64 { return s.Side * s.Side }
func main() {
var s Shape
fmt.Printf("%v %T %v\n", s, s, s == nil)
s = Rect{W: 2, H: 3}
fmt.Printf("%v %T %.1f\n", s, s, s.Area())
s = &Square{Side: 4}
fmt.Printf("%v %T %.1f\n", s, s, s.Area())
r := Rect{W: 1, H: 1}
s = r // r'nin bir kopyası arayüze konur
r.W = 10
fmt.Println(s.Area(), r.Area())
}<nil> <nil> true
{2 3} main.Rect 6.0
&{4} *main.Square 16.0
1 10var s Shape
+------------+-------------+
| tip: nil | değer: nil | s == nil -> true
+------------+-------------+
s = Rect{W: 2, H: 3}
+------------+-------------+
| tip: Rect | değer: {2 3}| s.Area() -> Rect.Area çağrılır
+------------+-------------+
s = &Square{Side: 4}
+--------------+-----------+ +--------------+
| tip: *Square | değer: *--+-------> | Square{4} |
+--------------+-----------+ +--------------+Programın son iki satırı önemli bir sonucu gösteriyor: Bir struct değerini arayüze atadığında arayüz o değerin kopyasını tutar. Sonradan r.W'yi değiştirmek arayüzdeki kopyayı etkilemez, bu yüzden s.Area() hâlâ 1 döndürür. Arayüze bir işaretçi atasaydın, kopyalanan şey adres olacağı için iki taraf aynı struct'ı görürdü.
Arayüzün nil olması, iki gözün de boş olması demektir. var s Shape ile tanımlanan değişken bu durumdadır. nil bir arayüz üzerinden metot çağırmak, çağrılacak metodu bulacak tip bilgisi olmadığı için panik oluşturur. Bu iki gözlü yapıyı aklında tut; birazdan nil arayüz tuzağında çok işine yarayacak.
Perde arkası: itab ve veri işaretçisi
Çalışma zamanında, metodu olan bir arayüz değeri iki kelimelik (iki işaretçi boyutunda) bir yapıdır. İlk kelime itab denen bir tabloyu gösterir. Bu tabloda dinamik tipin bilgisi ve o tipin, arayüzün metotlarına karşılık gelen fonksiyonlarının adresleri bulunur. İkinci kelime ise veriyi gösterir. Dinamik değer bir işaretçiyse ikinci kelime doğrudan o işaretçidir; değilse genellikle değerin bir kopyasını gösterir.
var s Shape = Rect{W: 2, H: 3}
s (Shape)
+--------------------+ itab (Shape, Rect)
| tab *------------+-------> +---------------------------+
| | | tip: main.Rect |
| | | fun[0]: main.Rect.Area |
| data *------------+--+ +---------------------------+
+--------------------+ |
+----> +---------------------------+
| Rect{W: 2, H: 3} (kopya) |
+---------------------------+s.Area() çağrısında çalışma zamanı itab'daki fonksiyon adresini okur ve veri işaretçisiyle birlikte çağırır. Bu dolaylı çağrının maliyeti genellikle çok küçüktür; ama derleyicinin fonksiyonu satır içine yerleştirmesini (inlining) engelleyebilir ve değerin heap'e kaçmasına yol açabilir. Metodu olmayan any tipinde metot tablosuna gerek olmadığı için ilk kelime doğrudan tip bilgisini gösterir.
any: boş arayüz
Hiç metodu olmayan arayüz interface{}'tir. Her tipin metot kümesi boş kümeyi kapsadığı için her tip bu arayüzü sağlar. Go 1.18 ile gelen any, interface{} için tanımlanmış bir takma addır (alias); ikisi tamamen aynı tiptir ve modern kodda any yazılır.
package main
import "fmt"
func main() {
values := []any{42, "merhaba", 3.14, true, []int{1, 2}, nil}
for _, v := range values {
// Sprint değeri önce metne çevirir; %-8v dilimlerde her elemanı ayrı hizalardı.
fmt.Printf("%-8s %T\n", fmt.Sprint(v), v)
}
}42 int merhaba string 3.14 float64 true bool [1 2] []int <nil> <nil>
fmt.Println(a ...any) gibi her türlü değeri kabul etmesi gereken fonksiyonlar, JSON verisini önceden bilinmeyen bir yapıya çözmek (map[string]any) ya da farklı tipleri aynı koleksiyonda tutmak any'nin tipik kullanım alanlarıdır.
Bedeli ise statik tip güvenliğini kaybetmektir. var x any = 42 yazdıktan sonra x + 1 derlenmez (mismatched types any and untyped int); çünkü derleyici x'in içinde ne olduğunu bilmez. İçerideki değere ulaşmak için tip iddiası veya tip switch kullanman gerekir. Go atasözlerinden (Go Proverbs) biri bunu kısaca özetler: "interface says nothing", yani boş arayüz hiçbir şey söylemez. Aynı kodu farklı tiplerle kullanmak istiyorsan çoğu zaman Jenerikler daha iyi bir araçtır.
Tip iddiası (type assertion) ve virgül-ok
Bir arayüz değerinin içindeki somut değere ulaşmak için tip iddiası kullanılır: x.(T). Bu ifade "x'in dinamik tipinin T olduğunu iddia ediyorum" der. İki biçimi vardır:
v := x.(T): İddia doğruysav,Ttipinde değeri alır. Yanlışsa program panik oluşturur.v, ok := x.(T): İddia yanlışsa panik olmaz;v,T'nin sıfır değerini,okisefalsedeğerini alır. Bu biçime virgül-ok (comma ok) deyimi denir.
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return 3.14 * c.R * c.R }
func main() {
var s Shape = Rect{W: 2, H: 5}
r := s.(Rect) // dinamik tip gerçekten Rect: başarılı
fmt.Println("genişlik:", r.W)
c, ok := s.(Circle) // dinamik tip Circle değil: panik yok
fmt.Println(c, ok)
if r, ok := s.(Rect); ok {
fmt.Println("dikdörtgen, yükseklik:", r.H)
}
}genişlik: 2
{0} false
dikdörtgen, yükseklik: 5Tek dönüş değerli biçimi yanlış tiple kullanırsan hata mesajı hem arayüzün hem de gerçek dinamik tipin adını söyler:
var s Shape = Rect{W: 2, H: 5}
c := s.(Circle) // panikpanic: interface conversion: main.Shape is main.Rect, not main.Circle
Tek değerli biçimi yalnızca tipten gerçekten emin olduğunda ve yanlış olmasının bir programlama hatası anlamına geldiği durumlarda kullan. Diğer her durumda virgül-ok biçimini tercih et.
T somut bir tip olmak zorunda değildir; bir arayüz de olabilir. w.(io.StringWriter) ifadesi "w'nin dinamik değeri WriteString metoduna da sahip mi?" diye sorar. Standart kütüphane bu yöntemle opsiyonel yetenekleri keşfeder: io.WriteString fonksiyonu, kendisine verilen Writer aynı zamanda WriteString metoduna sahipse onu kullanır, değilse string'i byte dilimine çevirip Write çağırır.
Tip switch
Birden fazla tipi kontrol etmen gerekiyorsa art arda tip iddiaları yazmak yerine tip switch kullanırsın. Sözdizimi normal switch'e benzer ama x.(type) özel ifadesini kullanır:
package main
import (
"errors"
"fmt"
"strings"
)
func describe(x any) string {
switch v := x.(type) {
case nil:
return "nil değer"
case int:
return fmt.Sprintf("int: %d, iki katı %d", v, v*2)
case string:
return fmt.Sprintf("string: %q, büyük harfle %q", v, strings.ToUpper(v))
case bool:
if v {
return "bool: doğru"
}
return "bool: yanlış"
case []int, []string:
return fmt.Sprintf("dilim: %v (burada v'nin tipi %T değil, any)", v, v)
case error:
return "hata: " + v.Error()
default:
return fmt.Sprintf("bilinmeyen tip: %T", v)
}
}
func main() {
inputs := []any{42, "gopher", true, []int{1, 2}, errors.New("dosya yok"), 3.5, nil}
for _, in := range inputs {
fmt.Println(describe(in))
}
}int: 42, iki katı 84 string: "gopher", büyük harfle "GOPHER" bool: doğru dilim: [1 2] (burada v'nin tipi []int değil, any) hata: dosya yok bilinmeyen tip: float64 nil değer
Tip switch'in birkaç inceliği var:
v'nin tipi case'e göre değişir. Tek tipli bir case'te (case int:)vo tiptedir, bu yüzdenv*2yazabildik. Birden çok tip listelenen case'te (case []int, []string:) derleyici hangi tipin geleceğini bilemez;vswitch edilen ifadenin tipinde, yanianyolarak kalır. Çıktıdaki%Tfiili ise statik tipi değil, içerideki dinamik tipi yazdırdığı için[]intgösterir.case nilyalnızca nil arayüz değerleriyle eşleşir.- Arayüz tipleri de case olabilir.
case error:Error metodu olan her dinamik tiple eşleşir. Case'ler yukarıdan aşağıya denenir ve ilk eşleşen kazanır; bu yüzden geniş arayüzleri daha özel tiplerden sonra yaz. - Değere ihtiyacın yoksa
switch x.(type)biçiminde değişken bağlamadan da yazabilirsin.
Standart kütüphanenin küçük arayüzleri
fmt.Stringer
fmt paketindeki Stringer arayüzü tek bir metottan oluşur:
type Stringer interface {
String() string
}fmt fonksiyonları bir değeri %v veya %s ile yazdırırken önce onun Stringer olup olmadığına bakar. Öyleyse String() metodunu çağırır. Bu, kendi tiplerinin nasıl görüneceğini belirlemenin standart yoludur:
package main
import "fmt"
type Money struct {
Cents int64
Currency string
}
func (m Money) String() string {
return fmt.Sprintf("%d,%02d %s", m.Cents/100, m.Cents%100, m.Currency)
}
type Point struct{ X, Y int }
func main() {
price := Money{Cents: 125050, Currency: "TL"}
fmt.Println(price)
fmt.Printf("Fiyat: %v | %s\n", price, price)
fmt.Println(Point{X: 1, Y: 2}) // String metodu yok: varsayılan biçim
// Arayüzden arayüze tip iddiası: bu değer Stringer mı?
var anything any = price
if s, ok := anything.(fmt.Stringer); ok {
fmt.Println("Stringer sağlanıyor:", s.String())
}
}1250,50 TL
Fiyat: 1250,50 TL | 1250,50 TL
{1 2}
Stringer sağlanıyor: 1250,50 TLfmt'nin bu davranışı da içeride bir tip iddiasından ibarettir: fmt, aldığı any değerinin Stringer olup olmadığını kontrol eder.
io.Reader
Go'da neredeyse bütün veri akışları iki tek metotlu arayüz etrafında şekillenir. Dosyalar, ağ bağlantıları, HTTP gövdeleri, sıkıştırılmış akışlar ve bellekteki tamponlar bu arayüzleri sağlar.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Read, p dilimine en fazla len(p) byte okur; okuduğu byte sayısını n olarak, varsa hatayı err olarak döndürür. Veri bittiğinde io.EOF döner. Belgelerdeki önemli bir ayrıntı: Read hem n > 0 hem de hata döndürebilir. Bu yüzden önce gelen n byte'ı işlemeli, sonra hataya bakmalısın. strings.NewReader bir string'i io.Reader'a dönüştürür; bununla sözleşmeyi adım adım görebiliriz:
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Merhaba, Gopher!")
buf := make([]byte, 5)
for {
n, err := r.Read(buf)
if n > 0 {
fmt.Printf("%d byte: %q\n", n, buf[:n])
}
if err == io.EOF {
fmt.Println("akışın sonu")
break
}
if err != nil {
fmt.Println("hata:", err)
break
}
}
}5 byte: "Merha" 5 byte: "ba, G" 5 byte: "opher" 1 byte: "!" akışın sonu
16 byte'lık metin 5 byte'lık bir tamponla dört seferde okundu. Beşinci çağrıda okunacak veri kalmadığı için Read 0 ve io.EOF döndürdü. Gerçek kodda bu döngüyü nadiren elle yazarsın; io.ReadAll, io.Copy ve bufio.Scanner gibi yardımcılar bunu senin yerine yapar.
io.Writer ve kendi Writer'ın
Write, p'deki len(p) byte'ın hepsini yazmaya çalışır ve yazdığı byte sayısını döndürür. n < len(p) ise nil olmayan bir hata döndürmek zorundadır. Ayrıca p'yi değiştirmemeli ve çağrı bittikten sonra saklamamalıdır.
Kendi io.Writer'ını yazmak için tek yapman gereken bu metodu tanımlamaktır. Aşağıdaki LineCounter kendisine yazılan byte'ları ve satırları sayar. fmt.Fprintf ve io.Copy gibi fonksiyonlar LineCounter'ı hiç tanımadıkları hâlde onunla sorunsuz çalışır:
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
)
// LineCounter, kendisine yazılan byte'ları ve satırları sayan bir io.Writer'dır.
type LineCounter struct {
Bytes int
Lines int
}
func (c *LineCounter) Write(p []byte) (int, error) {
c.Bytes += len(p)
c.Lines += bytes.Count(p, []byte("\n"))
return len(p), nil
}
func main() {
var counter LineCounter
fmt.Fprintf(&counter, "Ad: %s\n", "Gopher")
fmt.Fprintln(&counter, "Dil: Go")
src := strings.NewReader("bir\niki\nüç\n")
n, err := io.Copy(&counter, src)
fmt.Println("kopyalanan:", n, "hata:", err)
fmt.Printf("toplam %d byte, %d satır\n", counter.Bytes, counter.Lines)
// Aynı fonksiyonlar os.Stdout ile de çalışır; o da bir io.Writer
fmt.Fprintln(os.Stdout, "ekrana yazıldı")
}kopyalanan: 13 hata: <nil> toplam 32 byte, 5 satır ekrana yazıldı
Write metodu LineCounter'ın alanlarını değiştirdiği için işaretçi alıcıyla tanımlandı; bu yüzden io.Writer bekleyen yerlere counter değil &counter veriyoruz. "üç" kelimesindeki ü ve ç UTF-8'de ikişer byte tuttuğu için kopyalanan metin 13 byte'tır. Bu arayüzlerin gerçek dosyalarla kullanımını Dosyalar, I/O ve JSON dersinde göreceksin.
Klasik bir örnek: sort.Interface
Jeneriklerden önce Go'da her tipi sıralayabilmenin yolu sort paketinin arayüzüydü ve bugün hâlâ arayüz tasarımının öğretici bir örneğidir:
type Interface interface {
Len() int // eleman sayısı
Less(i, j int) bool // i. eleman j.'den önce mi gelmeli?
Swap(i, j int) // i. ve j. elemanları takas et
}Sıralama algoritması elemanların ne olduğunu hiç bilmez; yalnızca kaç eleman olduğunu sorar, ikisini karşılaştırır ve takas eder. Bu üç metodu sağlayan her şey sıralanabilir:
package main
import (
"cmp"
"fmt"
"slices"
"sort"
)
type User struct {
Name string
Age int
}
// byAge, []User için sort.Interface arayüzünü uygular.
type byAge []User
func (a byAge) Len() int { return len(a) }
func (a byAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a byAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
users := []User{{"Ayşe", 31}, {"Ali", 25}, {"Zeynep", 28}}
sort.Sort(byAge(users))
fmt.Println(users)
sort.Sort(sort.Reverse(byAge(users)))
fmt.Println(users)
// Modern alternatif: slices.SortFunc ve cmp.Compare
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Name, b.Name)
})
fmt.Println(users)
}[{Ali 25} {Zeynep 28} {Ayşe 31}]
[{Ayşe 31} {Zeynep 28} {Ali 25}]
[{Ali 25} {Ayşe 31} {Zeynep 28}]sort.Reverse da zarif bir numara kullanır: sort.Interface'i bir struct'ın içine gömer ve yalnızca Less metodunu, argümanların yerini değiştirerek yeniden tanımlar. Len ve Swap gömülü değerden olduğu gibi gelir. Yeni kodda slices.SortFunc genellikle daha kısa ve daha hızlıdır; sıralama algoritmalarının ayrıntılarını Verimli Sıralama Algoritmaları dersinde bulabilirsin.
nil arayüz tuzağı
Arayüz değerinin iki gözlü yapısını hatırla: Bir arayüz yalnızca hem tipi hem değeri nil olduğunda nil'e eşittir. Peki içine nil bir işaretçi konmuş bir arayüz nil midir? Hayır. Tip gözü dolu olduğu için arayüz nil değildir. Bu, Go'da en sık yaşanan ve en çok kafa karıştıran hatalardan biridir ve genellikle error dönüş değerlerinde ortaya çıkar:
package main
import "fmt"
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return e.Field + " alanı geçersiz"
}
// validate HATALI: nil bir *ValidationError'ı error olarak döndürür.
func validate(name string) error {
var err *ValidationError // nil işaretçi
if name == "" {
err = &ValidationError{Field: "name"}
}
return err // (tip: *ValidationError, değer: nil) olan bir arayüz döner
}
// validateFixed DOĞRU: hata yoksa açıkça nil döndürür.
func validateFixed(name string) error {
if name == "" {
return &ValidationError{Field: "name"}
}
return nil
}
func main() {
err := validate("Gopher")
fmt.Println("validate:", err == nil)
fmt.Printf("tip: %T, değer: %v\n", err, err)
err = validateFixed("Gopher")
fmt.Println("validateFixed:", err == nil)
}validate: false tip: *main.ValidationError, değer: <nil> validateFixed: true
Çıktının ikinci satırı durumu iyice kafa karıştırıcı hâle getiriyor: Değeri yazdırınca <nil> görüyorsun ama err == nil yine de false. fmt, nil işaretçi üzerinde Error() çağrısı paniğe dönüşünce bunu yakalayıp <nil> yazdırır. Oysa karşılaştırma iki gözü birlikte kontrol eder:
validateFixed dönüşü: validate dönüşü:
error error
+-----------+------------+ +-----------------------+------------+
| tip: nil | değer: nil | | tip: *ValidationError | değer: nil |
+-----------+------------+ +-----------------------+------------+
err == nil -> true err == nil -> falseHatalı sürümü kullanan çağıran taraf if err != nil kontrolünden geçer, hata olduğunu sanır ve err.Error() çağırırsa nil işaretçi paniğiyle karşılaşır. Bu tuzağa düşmemek için:
- Hata olmadığında her zaman açıkça
nildöndür (return nil). - Arayüz döndüren bir fonksiyonda somut işaretçi tipinde bir ara değişken (
var err *ValidationError) kullanma. - Fonksiyonların dönüş tipini
*ValidationErrordeğilerroryap.
Arayüz gömme
Tıpkı struct'larda olduğu gibi arayüzleri de birbirinin içine gömebilirsin (embedding). Gömülen arayüzlerin metotları yeni arayüzün metot kümesine eklenir. io paketi bunu yoğun şekilde kullanır:
type ReadWriter interface {
Reader
Writer
}
type ReadCloser interface {
Reader
Closer
}Böylece önce küçük yapı taşları tanımlanır, sonra ihtiyaca göre birleştirilir. Kendi şekil örneğimize bir ad yeteneği ekleyelim:
package main
import "fmt"
type Shape interface {
Area() float64
Perimeter() float64
}
type Namer interface {
Name() string
}
// NamedShape, iki arayüzü gömerek metotlarını birleştirir.
type NamedShape interface {
Shape
Namer
}
type Room struct {
Label string
Width, Length float64
}
func (r Room) Area() float64 { return r.Width * r.Length }
func (r Room) Perimeter() float64 { return 2 * (r.Width + r.Length) }
func (r Room) Name() string { return r.Label }
func totalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func main() {
rooms := []NamedShape{
Room{Label: "Salon", Width: 4, Length: 5.5},
Room{Label: "Mutfak", Width: 3, Length: 3.5},
}
shapes := make([]Shape, 0, len(rooms))
for _, r := range rooms {
fmt.Printf("%-7s %5.1f m² (çevre %4.1f m)\n", r.Name(), r.Area(), r.Perimeter())
shapes = append(shapes, r) // NamedShape, Shape'e örtük olarak dönüşür
}
fmt.Printf("%-7s %5.1f m²\n", "Toplam", totalArea(shapes))
}Salon 22.0 m² (çevre 19.0 m) Mutfak 10.5 m² (çevre 13.0 m) Toplam 32.5 m²
Daha geniş bir arayüz değeri (NamedShape), daha dar bir arayüz bekleyen yere (Shape) doğrudan atanabilir. Ama dikkat: []NamedShape dilimini []Shape bekleyen totalArea'ya doğrudan veremezsin. İki dilimin bellekteki elemanları farklı arayüz değerleridir ve Go bu dönüşümü örtük olarak yapmaz; bu yüzden elemanları döngüyle yeni bir dilime aktardık.
Tasarım ilkeleri
Küçük arayüzler
Go atasözlerinden bir diğeri şöyledir: "The bigger the interface, the weaker the abstraction", yani arayüz büyüdükçe soyutlama zayıflar. Standart kütüphanenin en çok kullanılan arayüzleri bir veya iki metotludur: io.Reader, io.Writer, io.Closer, fmt.Stringer, error. Tek metotlu arayüzlere genellikle metodun adına -er eki getirilerek isim verilir.
Küçük arayüzü sağlamak kolaydır, test için sahte (fake) bir sürümünü yazmak kolaydır ve birbiriyle birleştirilebilir. On metotlu bir arayüzü ise tek bir tipten başka kimse sağlamaz; o noktada arayüz soyutlama olmaktan çıkar ve somut tipin bir kopyası hâline gelir.
Arayüzü kullanan taraf tanımlar
Java gibi dillerde arayüz genellikle onu uygulayan sınıfın yanına yazılır. Go'da örtük uygulama sayesinde tersi yaygındır: Arayüz, onu kullanan pakette ve o paketin tam olarak ihtiyaç duyduğu metotlarla tanımlanır. Bir rapor servisinin yalnızca veri kaydetmesi gerekiyorsa, veritabanı paketinin yirmi metotlu tipine bağlanmak yerine kendi içinde tek metotlu bir arayüz tanımlar:
// report paketi: yalnızca ihtiyacı olanı ister.
type Saver interface {
Save(r Report) error
}
func Publish(s Saver, r Report) error {
// ...
return s.Save(r)
}Tek bir uygulaması olan ve "belki ileride lazım olur" diye önceden yazılmış arayüzlerden kaçın. Arayüzü ikinci bir uygulamaya (gerçek bir alternatif veya test için sahte bir sürüm) gerçekten ihtiyaç duyduğunda çıkarmak Go'da çok kolaydır, çünkü mevcut tiplere dokunman gerekmez.
Arayüz kabul et, struct döndür
Go topluluğunda sık tekrarlanan bir öneri, fonksiyonların parametre olarak arayüz kabul etmesi, dönüş değeri olarak ise somut tip döndürmesidir.
- Arayüz kabul et: Fonksiyon, ihtiyacı olan en dar arayüzü istesin.
*os.Fileyerineio.Readeralan bir fonksiyon dosyalarla, ağ bağlantılarıyla, sıkıştırılmış akışlarla ve testlerdestrings.NewReaderile çalışır. - Struct döndür:
NewBuffergibi yapıcılar*bytes.Buffergibi somut bir tip döndürsün. Çağıran tipin bütün metotlarına erişir ve hangi arayüz üzerinden kullanacağına kendisi karar verir.
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
// countWords herhangi bir kaynaktan okur: dosya, ağ bağlantısı, string...
func countWords(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanWords)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
func main() {
n, err := countWords(strings.NewReader("Arayüz kabul et, struct döndür."))
fmt.Println(n, err)
}5 <nil>
Bu kuralın bilinen istisnaları da var: error her zaman arayüz olarak döndürülür ve bir fabrika fonksiyonunun duruma göre farklı uygulamalar döndürmesi gerekiyorsa arayüz döndürmek doğaldır. Kuralı mekanik bir yasak değil, esnekliği doğru tarafa bırakmanın bir yolu olarak düşün.
Sık yapılan hatalar
- nil işaretçiyi arayüz olarak döndürmek.
var err *MyError; return errnil olmayan birerrorüretir. Hata yoksa her zaman açıkçareturn nilyaz. - İşaretçi alıcılı metotları olan bir değeri arayüze atamak.
Square does not implement Shape (method Area has pointer receiver)hatası alırsan değer yerine işaretçi ver:&Square{...}. - Arayüze işaretçi kullanmak.
*io.Readerveya*Shapeneredeyse hiçbir zaman gerekmez. Arayüz değeri zaten içinde bir işaretçi tutabilir; arayüze işaretçi yalnızca kodu karmaşıklaştırır. []Rect'i[]Shapesanmak. Somut tip dilimleri arayüz dilimlerine örtük olarak dönüşmez; elemanları döngüyle aktarman gerekir.- Emin olmadan tek değerli tip iddiası kullanmak.
v := x.(T)yanlış tipte panik oluşturur. Tip garantili değilsev, ok := x.(T)veya tip switch kullan. - Karşılaştırılamayan dinamik tipleri karşılaştırmak. İçinde dilim veya map bulunan iki
anydeğerini==ile karşılaştırmak derlenir ama çalışma zamanındacomparing uncomparable typepaniği oluşturur. Aynısımap[any]Tanahtarları için de geçerlidir. - Her tip için önceden arayüz yazmak.
UserServiceInterfacegibi tek uygulamalı, büyük arayüzler Go'da nadiren işe yarar. Arayüzü ihtiyaç doğduğunda, kullanan tarafta ve küçük tanımla. any'yi jenerik yerine kullanmak.func Max(a, b any) anytip güvenliğini kaybettirir ve her çağrıda tip iddiası gerektirir; bu tür durumlar için tip parametreleri vardır.
Alıştırmalar
Derste yazdığımız Shape arayüzüne (Area ve Perimeter) uyan bir Triangle tipi ekle. Üçgen, üç kenar uzunluğuyla (A, B, C) tanımlansın ve alanı Heron formülüyle hesaplansın: s = (A + B + C) / 2 olmak üzere alan √(s(s−A)(s−B)(s−C)).
Ardından:
RectveTriangle'ınShape'i sağladığını derleme zamanında garanti eden satırları yaz.largest(shapes []Shape) Shapefonksiyonunu yaz: En büyük alanlı şekli döndürsün, dilim boşsa nil döndürsün.
İpucu
Karekök için math.Sqrt kullan. largest içinde var best Shape ile başlayıp best == nil kontrolüyle ilk elemanı ele alabilirsin.
Çözümü göster
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Rect struct{ Width, Height float64 }
func (r Rect) Area() float64 { return r.Width * r.Height }
func (r Rect) Perimeter() float64 { return 2 * (r.Width + r.Height) }
type Triangle struct{ A, B, C float64 }
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
// Area, Heron formülünü kullanır.
func (t Triangle) Area() float64 {
s := t.Perimeter() / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
// Derleme zamanı kontrolleri
var (
_ Shape = Rect{}
_ Shape = Triangle{}
)
// largest, en büyük alanlı şekli döndürür; dilim boşsa nil döner.
func largest(shapes []Shape) Shape {
var best Shape
for _, s := range shapes {
if best == nil || s.Area() > best.Area() {
best = s
}
}
return best
}
func main() {
shapes := []Shape{
Rect{Width: 2, Height: 3},
Triangle{A: 5, B: 5, C: 6},
Rect{Width: 1, Height: 5},
}
best := largest(shapes)
fmt.Printf("%T %v alan: %.1f\n", best, best, best.Area())
fmt.Println(largest(nil))
}main.Triangle {5 5 6} alan: 12.0
<nil>5-5-6 kenarlı üçgen için s = 8 ve alan √(8·3·3·2) = 12 olur. var _ Shape = Triangle{} satırı, Triangle'dan bir metodu silersen programın derlenmemesini sağlar. largest(nil) için döngü hiç çalışmaz ve best, sıfır değeri olan nil arayüz olarak döner. Burada nil tuzağı yoktur, çünkü best en baştan arayüz tipinde tanımlandı.
Player tipi için sort.Interface'i uygulayan byScore tipini yaz. Oyuncular yüksek skordan düşüğe sıralansın; skorları eşit olan oyuncular isimlerine göre alfabetik sıralansın. Aynı sıralamayı slices.SortFunc ile de yap ve iki sonucun aynı olduğunu göster.
players := []Player{
{"Can", 80}, {"Ada", 95}, {"Ece", 80}, {"Bora", 95}, {"Deniz", 70},
}İpucu
Less(i, j) içinde önce skorları karşılaştır; farklıysa p[i].Score > p[j].Score döndür. Eşitse isimleri < ile karşılaştır. slices.SortFunc için cmp.Or, sıfır olmayan ilk karşılaştırma sonucunu döndürür.
Çözümü göster
package main
import (
"cmp"
"fmt"
"slices"
"sort"
)
type Player struct {
Name string
Score int
}
type byScore []Player
func (p byScore) Len() int { return len(p) }
func (p byScore) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p byScore) Less(i, j int) bool {
if p[i].Score != p[j].Score {
return p[i].Score > p[j].Score // yüksek skor önce
}
return p[i].Name < p[j].Name // eşitlikte isme göre
}
var _ sort.Interface = byScore(nil)
func main() {
players := []Player{
{"Can", 80}, {"Ada", 95}, {"Ece", 80}, {"Bora", 95}, {"Deniz", 70},
}
other := slices.Clone(players)
sort.Sort(byScore(players))
for i, p := range players {
fmt.Printf("%d. %-5s %d\n", i+1, p.Name, p.Score)
}
slices.SortFunc(other, func(a, b Player) int {
return cmp.Or(
cmp.Compare(b.Score, a.Score), // azalan skor
cmp.Compare(a.Name, b.Name), // artan isim
)
})
fmt.Println("aynı sonuç:", slices.Equal(players, other))
}1. Ada 95 2. Bora 95 3. Can 80 4. Ece 80 5. Deniz 70 aynı sonuç: true
Less metodunun iki anahtarlı karşılaştırması tüm sıralamayı belirler; algoritma oyuncular hakkında başka hiçbir şey bilmez. var _ sort.Interface = byScore(nil) satırı üç metodun da doğru imzayla yazıldığını derleme anında garanti eder. slices.SortFunc sürümünde cmp.Compare(b.Score, a.Score) argümanların yeri değiştirildiği için azalan sıra verir. slices.Equal ise iki dilimi eleman eleman karşılaştırır. Sıralama O(n log n) zamanda çalışır.
encoding/json bir JSON belgesini any içine çözdüğünde yalnızca şu tipleri üretir: nil, bool, float64, string, []any ve map[string]any. Bu değerleri JSON'a benzer tek satırlık bir metne dönüştüren özyinelemeli format(v any) string fonksiyonunu yaz.
niliçinnull, string'ler için tırnaklı metin, sayılar için gereksiz sıfırlar olmadan sayı yazılsın.- Map anahtarları çıktının her çalıştırmada aynı olması için sıralı yazılsın.
- Desteklenmeyen bir tip gelirse
<desteklenmeyen tip int>gibi bir metin döndürülsün.
İpucu
Her case'te v doğru tipe sahip olacak. Dilim ve map case'lerinde elemanlar için format'ı yeniden çağır. strconv.Quote, strconv.FormatFloat(x, 'f', -1, 64) ve slices.Sorted(maps.Keys(m)) işine yarayacak.
Çözümü göster
package main
import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
)
func format(v any) string {
switch x := v.(type) {
case nil:
return "null"
case bool:
return strconv.FormatBool(x)
case float64:
return strconv.FormatFloat(x, 'f', -1, 64)
case string:
return strconv.Quote(x)
case []any:
parts := make([]string, len(x))
for i, item := range x {
parts[i] = format(item)
}
return "[" + strings.Join(parts, ", ") + "]"
case map[string]any:
keys := slices.Sorted(maps.Keys(x))
parts := make([]string, len(keys))
for i, k := range keys {
parts[i] = strconv.Quote(k) + ": " + format(x[k])
}
return "{" + strings.Join(parts, ", ") + "}"
default:
return fmt.Sprintf("<desteklenmeyen tip %T>", x)
}
}
func main() {
data := map[string]any{
"ad": "Gopher",
"yaş": 16.0,
"aktif": true,
"puan": 9.5,
"adres": nil,
"diller": []any{"Go", "C", map[string]any{"favori": true}},
}
fmt.Println(format(data))
fmt.Println(format(42))
}{"ad": "Gopher", "adres": null, "aktif": true, "diller": ["Go", "C", {"favori": true}], "puan": 9.5, "yaş": 16}
<desteklenmeyen tip int>Tip switch her case'te x'i doğru tipe dönüştürdüğü için x bir float64 iken sayı, []any iken dilim gibi kullanılabiliyor. Özyineleme sayesinde iç içe geçmiş dilim ve map'ler de doğal olarak işleniyor. 42 sabiti int tipinde olduğu için float64 case'ine girmez ve default dalına düşer; JSON çözücü asla int üretmediği için bu beklenen bir durumdur. Map'lerin gezinme sırası rastgele olduğundan anahtarları sıralamak zorundayız.
Başka bir io.Writer'ı saran ve her satırın başına satır numarası ekleyen LineNumberWriter tipini yaz. NewLineNumberWriter(w io.Writer) *LineNumberWriter yapıcısıyla oluşturulsun.
Zorlayıcı kısım şu: Bir satır birden fazla Write çağrısına bölünebilir, bir Write çağrısı da birden çok satır içerebilir. Numara yalnızca gerçekten yeni bir satırın başında yazılmalı. Ayrıca Write, başarılı olduğunda kendisine verilen p'nin uzunluğunu döndürmeli; eklediğin numara byte'larını saymamalı.
İpucu
Tipin içinde, son yazılan byte'ın satır sonu olup olmadığını tutan bir durum alanı sakla (örneğin midLine bool). Write içinde p'yi bytes.IndexByte(p, '\n') ile parçalara ayır. Her parçadan önce, satır başındaysan numarayı yaz.
Çözümü göster
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
)
type LineNumberWriter struct {
w io.Writer
line int
midLine bool // son yazılan byte satır sonu değilse true
}
func NewLineNumberWriter(w io.Writer) *LineNumberWriter {
return &LineNumberWriter{w: w}
}
func (l *LineNumberWriter) Write(p []byte) (int, error) {
written := 0
for len(p) > 0 {
if !l.midLine {
l.line++
if _, err := fmt.Fprintf(l.w, "%d: ", l.line); err != nil {
return written, err
}
l.midLine = true
}
// Bir sonraki satır sonuna kadar olan parçayı (satır sonu dahil) seç.
chunk := p
if i := bytes.IndexByte(p, '\n'); i >= 0 {
chunk = p[:i+1]
}
n, err := l.w.Write(chunk)
written += n
if err != nil {
return written, err
}
if chunk[len(chunk)-1] == '\n' {
l.midLine = false
}
p = p[len(chunk):]
}
return written, nil
}
func main() {
lw := NewLineNumberWriter(os.Stdout)
fmt.Fprint(lw, "Birinci satır\nİkinci ")
fmt.Fprint(lw, "satır\nÜçüncü satır\n")
if _, err := io.Copy(lw, strings.NewReader("dördüncü\nbeşinci\n")); err != nil {
fmt.Println("hata:", err)
}
}1: Birinci satır 2: İkinci satır 3: Üçüncü satır 4: dördüncü 5: beşinci
"İkinci satır" iki ayrı Write çağrısına bölünmesine rağmen tek numara aldı, çünkü midLine alanı çağrılar arasında durumu taşıyor. Bu yüzden Write işaretçi alıcıyla tanımlandı ve yapıcı *LineNumberWriter döndürüyor; bu, "struct döndür" ilkesinin de bir örneği. written yalnızca p'den yazılan byte'ları sayar: io.Writer sözleşmesine göre dönüş değeri çağıranın verdiği veriye göre anlam taşır. Her byte bir kez işlendiği için zaman karmaşıklığı O(len(p))'dir. LineNumberWriter, os.Stdout yerine bir dosya veya bytes.Buffer ile de hiçbir değişiklik yapmadan çalışır.
Kısa sınav
type Shape interface{ Area() float64 } ve func (c *Circle) Area() float64 tanımlıyken var s Shape = Circle{} satırı için ne olur?
func find() error { var e *MyErr; return e } tanımlıyken fmt.Println(find() == nil) ne yazdırır?
var v any = 3.0; n, ok := v.(int); fmt.Println(n, ok) ne yazdırır?
switch v := x.(type) { case []int, []string: ... } içinde v'nin statik tipi nedir?
Hangi imza "arayüz kabul et, struct döndür" önerisine en iyi uyar?
var a, b any = []int{1}, []int{1} tanımlıyken fmt.Println(a == b) ne olur?
Özet
- Arayüz, metot imzalarından oluşan bir sözleşmedir. Gerekli metotlara sahip her tip arayüzü örtük olarak sağlar ve kontrol derleme anında yapılır.
- İşaretçi alıcılı metotlar yalnızca
*T'nin metot kümesindedir.var _ Shape = (*Square)(nil)ile uyumu derleme anında garanti edebilirsin. - Arayüz değeri bir (dinamik tip, dinamik değer) çiftidir. Arayüze konan değer kopyalanır ve arayüz yalnızca iki göz de boşsa nil'dir.
anyher değeri tutabilir ama tip bilgisini gizler. İçerideki değere tip iddiası (v, ok := x.(T)) veya tip switch ile ulaşılır; virgül-ok biçimi panik oluşturmaz.fmt.Stringer,io.Reader,io.Writervesort.Interfacegibi küçük arayüzler, birbirini tanımayan kod parçalarının birlikte çalışmasını sağlar.- nil bir işaretçiyi arayüz olarak döndürmek nil olmayan bir arayüz üretir. Hata yoksa her zaman açıkça
return nilyaz. - Arayüzleri gömerek küçük parçalardan büyük sözleşmeler kurabilirsin; ama arayüzü küçük tut ve onu kullanan tarafta tanımla.
- Fonksiyonların ihtiyaç duydukları en dar arayüzü kabul etsin, yapıcılar ise somut tipler döndürsün.