Ders 06 / 13
Hata Arayüzü
Hata Go'da sıradan bir değerdir ve error arayüzünü örtük karşılar: Error() string yazan her tip, kaynağında bunu hiç söylemeden hata olur. Üç tip bunu doğrular, dinamik tipleri %T ile okunur; değer alıcılı bir hatanın yapısal eşitliği işaretçi alıcılı bir hatanın kimlik eşitliğinden ayrılır. Sınırlayıcı ölçüm: nil bir işaretçi error arayüzüne konduğunda arayüzün kendisi nil değildir.
İçindekiler
Önceki konunun son dersi bir tüketicinin, üreten tarafın kaynağında hiç yazmadığı bir arayüzü
tanımlayabildiğini ölçtü — bağın kaynakta hiç yazılmamasının doğal bir sonucuydu bu. Bu ders
aynı modelin Go’da en çok kullanıldığı yere bakıyor: standart kitaplığın kendisi tek bir arayüz
etrafında kurulu, error, ve her hata değeri onu tam olarak aynı biçimde, hiçbir bildirim
yazmadan karşılıyor.
error, Go’da sıradan bir dönüş değeridir ve sıradan bir arayüzdür — tek yöntemi var:
Error() string. Bu dersin sorusu, önceki konudan taşınan aynı soru: bu arayüzü karşılama bağı
nerede kuruluyor? Yanıtı bulmak için üç ayrı tip, hiçbirinin kaynağında error sözcüğü
geçmeden, hata olarak dönecek.
Fonksiyonlar dersi (M09/K01) errori zaten sıradan bir dönüş değeri olarak ölçtü ve çağıranın
onu hiç yakalamadan yok saymasının hiçbir şekilde reddedilmediğini gösterdi — o gözlem burada
tekrar kurulmuyor. Bu ders, o dönüş değerinin ne olduğuna, ve bir tipin ne zaman “bir hata”
sayıldığına bakıyor. Sorulan soru, kursun geri kalanında da aynı kalacak: bir bağ kaynakta açıkça
mı yazılı, derleyicinin baktığı kullanım yerinde mi kuruluyor, yoksa yalnız çalışma zamanında
değerin içinde mi taşınıyor?
Bu konu bu soruyu dört adımda ilerletiyor. Bu ders hatanın neden bir değer olduğunu ve arayüzü nasıl karşıladığını ölçüyor; sıradaki ders bir hatanın kimliğinin nerede durduğunu, üçüncü ders bir hataya eklenen bağlamın zincir boyunca nasıl korunduğunu, dördüncü ders de imzada hiç görünmeyen bir çıkış yolunun — paniğin — bağının nerede kurulduğunu (ya da kurulmadığını) ölçüyor.
Üç Tip, Sıfır Bildirim
Önceki konu örtük karşılamayı Arayüz Kavramı dersinde tek bir tip üzerinde ölçtü; bu ders aynı
düzeneği hata değerlerine uyguluyor. Aşağıdaki üç tip birbirinden bağımsız yazıldı: biri değer
alıcılı bir yapı, biri işaretçi alıcılı bir yapı, biri de tamsayı üzerine kurulu adlandırılmış
bir tip. Üçünün de ortak noktası tek satır: Error() string imzasına uyan bir yöntem.
Üç tipin özellikle farklı biçimlerde seçilmesi rastgele değil. error arayüzü standart
kitaplıkta o kadar yaygın kullanılıyor ki, hemen her tip biçimi — değer, işaretçi, adlandırılmış
temel tip, hatta boş bir yapı — onu karşılayabilir hâle geliyor. Ölçümün amacı, bu çeşitliliğin
karşılamayı hiç etkilemediğini göstermek: arayüz tek bir yöntem istiyor, o yöntemi hangi tipin
nasıl taşıdığıyla ilgilenmiyor.
// bag.go — hata bagi kullanim yerinde mi kuruluyor, degerin icinde mi
package main
import "fmt"
var kullanimYeri, deger int
func gozle(ad, yer, kanit string) {
switch yer {
case "kullanim":
kullanimYeri++
default:
deger++
}
fmt.Printf("%-40s %-9s %s\n", ad, yer, kanit)
}
func ozet() {
fmt.Printf("bag: %d kullanim yeri, %d deger\n", kullanimYeri, deger)
}
// olcum.go — hata bir deger: uc tip ondan hic bahsetmeden Error() string yazip hata oluyor
package main
import "fmt"
type BicimHatasi struct{ Alan string }
func (b BicimHatasi) Error() string { return "bicim gecersiz: " + b.Alan }
type ErisimHatasi struct{ Kod int }
func (e *ErisimHatasi) Error() string { return fmt.Sprintf("erisim reddedildi: kod %d", e.Kod) }
type SayacHatasi int
func (s SayacHatasi) Error() string { return fmt.Sprintf("sayac tasti: %d", int(s)) }
func main() {
var h1 error = BicimHatasi{Alan: "ad"}
var h2 error = &ErisimHatasi{Kod: 403}
var h3 error = SayacHatasi(9)
gozle("uc tipin hata arayuzunu karsilamasi", "kullanim",
"kaynaklarinda 'error' sozcugu hic gecmiyor")
gozle("hata degerinin dinamik tipi", "deger", fmt.Sprintf("%T, %T, %T", h1, h2, h3))
var a error = BicimHatasi{Alan: "ad"}
var b error = BicimHatasi{Alan: "ad"}
gozle("deger alicili iki hatanin esitligi", "deger", fmt.Sprint(a == b))
var c error = &ErisimHatasi{Kod: 403}
var d error = &ErisimHatasi{Kod: 403}
gozle("isaretci alicili iki hatanin esitligi", "deger", fmt.Sprint(c == d))
ozet()
}
uc tipin hata arayuzunu karsilamasi kullanim kaynaklarinda 'error' sozcugu hic gecmiyor hata degerinin dinamik tipi deger main.BicimHatasi, *main.ErisimHatasi, main.SayacHatasi deger alicili iki hatanin esitligi deger true isaretci alicili iki hatanin esitligi deger false bag: 1 kullanim yeri, 3 deger
HT1. Üç tipten hiçbiri error sözcüğünü kaynağında hiç anmıyor; BicimHatasi,
ErisimHatasi ve SayacHatasi yalnız Error() string imzasına uyan bir yöntem yazıyor ve bu
onları hata yapmaya yetiyor. Bağ, var h1 error = BicimHatasi{...} satırında, değer bir
error değişkenine konduğu anda kuruluyor — kaynakta hiçbir yerde “bu tip hata arayüzünü
karşılıyor” yazmıyor. %T ile okunan dinamik tip, üçünün de karşılamayı gerçekten
tamamladığını doğruluyor: değer tipi main.BicimHatasi olarak, işaretçi tipi
*main.ErisimHatasi olarak, adlandırılmış tip de main.SayacHatasi olarak görünüyor.
HT2. Son iki satır aynı BicimHatasi değerinden iki bağımsız kopya, aynı ErisimHatasi
değerinden de iki bağımsız işaretçi üretip karşılaştırıyor. Değer alıcılı hata yapısal
karşılaştırılıyor: iki ayrı değişken, alanları aynı olduğu için eşit çıkıyor. İşaretçi alıcılı
hata ise kimlik karşılaştırılıyor: iki ayrı &ErisimHatasi{...} çağrısı iki ayrı adres
üretiyor ve eşitlik false dönüyor, alanları birebir aynı olsa bile. Bu fark rastgele değil —
sıradaki ders paket düzeyinde tanımlanan bir hata değerinin kimliğini tam olarak bu ayrımın
üzerine kuruyor.
Bu iki satır aynı zamanda küçük bir uyarı taşıyor: error arayüzünün karşılaştırma operatörünü
(==) desteklemesi, iki hatanın “aynı hata” olduğu anlamına gelmiyor — yalnızca arayüzün
taşıdığı çiftin (dinamik tip, dinamik değer) iki tarafta da eşit olduğu anlamına geliyor. Değer
alıcılı bir hata için bu, alanların eşitliğine indirgeniyor; alanlardan biri bile farklıysa
karşılaştırma false dönüyor. Bu ayrım aşağıdaki bölümde, aynı yöntemin hangi tipin yöntem
kümesine girdiği sorusuyla birleşiyor.
Yöntem Kümesi: Karşılayan Değer Değil Tiptir
Önceki Konu’nun İşaretçiler dersi, bir yöntemin değer alıcı mı işaretçi alıcı mı olduğunun
çağrı yazımından değil alıcı tanımından geldiğini ölçtü — o mekanik burada tekrar kurulmuyor.
Ama bu dersin sorusu için tek bir sonucu önemli: ErisimHatasinin Error yöntemi işaretçi
alıcılı yazıldığında, hangi tip gerçekten error arayüzünü karşılıyor — ErisimHatasinin
kendisi mi, yoksa yalnız *ErisimHatasi mi? Yanıtı derleyiciye sormak, K01’in derlenirMi
aracını ödünç almayı gerektiriyor: bir kaynak parçası geçici bir pakete yazılıyor, derleniyor
ve yalnız geçip geçmediği okunuyor.
Bu araç yalnızca geçip geçmediğini okuyor, hata metnini hiç okumuyor — ölçülen şey “neden
reddedildi” değil, “reddedildi mi”. İki kaynak parçası da aynı ErisimHatasi tanımını ve aynı
boş işlev gövdesini paylaşıyor; aralarındaki tek fark, error değişkenine konan değerin .
işareti taşıyıp taşımadığı. Bu, arayüz karşılamasının sözdiziminde ne kadar küçük bir ayrıma
dayanabildiğini de gösteriyor.
// yontemkumesi.go — isaretci alicili yontem, sadece isaretci tipini hata arayuzune sokuyor
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
func derlenirMi(kaynak string) bool {
dizin, err := os.MkdirTemp("", "sinama")
if err != nil {
return false
}
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
const tipler = `
package sinama
type ErisimHatasi struct{ Kod int }
func (e *ErisimHatasi) Error() string { return "erisim reddedildi" }
`
func main() {
degerAtama := tipler + "func F() { var _ error = ErisimHatasi{Kod: 1} }"
isaretciAtama := tipler + "func F() { var _ error = &ErisimHatasi{Kod: 1} }"
gozle("deger tipini error degiskenine atama", "kullanim", fmt.Sprint(derlenirMi(degerAtama)))
gozle("isaretci tipini error degiskenine atama", "kullanim", fmt.Sprint(derlenirMi(isaretciAtama)))
ozet()
}
deger tipini error degiskenine atama kullanim false isaretci tipini error degiskenine atama kullanim true bag: 2 kullanim yeri, 0 deger
HT3. ErisimHatasi değeri, errore atanmaya çalıştığında derlemeyi durduruyor;
*ErisimHatasi işaretçisi aynı satırda sorunsuz geçiyor. İkisi de aynı alanları taşıyor,
ikisi de aynı Kod değerine sahip olabilir — ayrım verinin kendisinde değil, hangi tipin
Error() string yöntemine sahip olduğunda. Bir yöntem işaretçi alıcılı yazıldığında, o
yöntem yalnızca işaretçi tipinin yöntem kümesine giriyor; değer tipinin yöntem kümesi
onu hiç içermiyor.
HT4. Bu, örtük karşılamanın nerede kurulduğunu daha da netleştiriyor: bağ yalnızca
“bir Error() string yöntemi var mı” sorusuna değil, “bu tam tipin yöntem kümesinde bu
yöntem var mı” sorusuna bakıyor. Derleyici bunu her var _ error = ... satırında, atamanın
yapıldığı kullanım yerinde denetliyor — kaynakta ErisimHatasi implements error gibi bir
bildirim hiç yer almadığı hâlde, denetim gerçek ve iki farklı sonuç üretiyor.
Bu ayrımın pratik bir sonucu var: bir tipin hata yöntemini işaretçi alıcıyla yazmak, o tipin
yalnızca işaretçi biçiminin hata sayılacağı anlamına geliyor. Bir işlev ErisimHatasi{...}
değerini doğrudan error olarak döndürmeye çalışırsa derleme burada durur; işlevin
&ErisimHatasi{...} döndürmesi gerekiyor. Bu seçim rastgele değil — büyük bir yapının her
döndürülüşünde kopyalanmasını önlemek isteyen bir tasarım, aynı zamanda o yapının yalnızca
işaretçi biçiminin hata arayüzüne girmesini de seçmiş oluyor.
Sınırlayıcı Ölçüm: Dolu Bir Arayüz nil Görünmez
Bir hata dönmeyen yolun döndürdüğü de bir error değeridir — genellikle nil. Ama bu nilin
nereden geldiği önemli. Aşağıdaki dogrula işlevi somut bir işaretçi tipi (*AlanHatasi)
döndürüyor ve alan doluysa bu işaretçi gerçekten nil. Sorun, bu somut değerin bir error
değişkenine konduğu satırda başlıyor — üstelik bu satır kontrolEt işlevinin dönüş ifadesi
kadar sıradan görünüyor.
Bu örnek, önceki iki bölümde ölçülen iki gözlemi tek bir satırda birleştiriyor: AlanHatasinin
Error yöntemi işaretçi alıcılı, dolayısıyla yalnızca *AlanHatasi errori karşılıyor —
tıpkı ErisimHatasi gibi. Ve arayüz değeri, dinamik tip ile dinamik değeri birlikte taşıyan bir
çift — tıpkı ilk bölümde ölçülen üç tip gibi. Fark, bu kez dinamik değerin kendisinin nil
olması ve bunun arayüzün bütününü etkilememesi.
// tuzak.go — nil isaretcili somut hata, error arayuzune konunca artik nil degil
package main
import "fmt"
type AlanHatasi struct{ Alan string }
func (a *AlanHatasi) Error() string { return "alan gecersiz: " + a.Alan }
func dogrula(alan string) *AlanHatasi {
if alan == "" {
return &AlanHatasi{Alan: "zorunlu"}
}
return nil
}
func kontrolEt(alan string) error {
return dogrula(alan)
}
func main() {
somut := dogrula("ad")
hata := kontrolEt("ad")
gozle("somut isaretcinin kendisi", "deger", fmt.Sprint(somut == nil))
gozle("ayni degeri tasiyan hata arayuzu", "deger", fmt.Sprint(hata == nil))
gozle("hata arayuzunun dinamik tipi", "deger", fmt.Sprintf("%T", hata))
ozet()
}
somut isaretcinin kendisi deger true ayni degeri tasiyan hata arayuzu deger false hata arayuzunun dinamik tipi deger *main.AlanHatasi bag: 0 kullanim yeri, 3 deger
HT5. dogrula("ad") çağrısı gerçekten nil bir işaretçi döndürüyor — üçüncü satır bunu
doğruluyor. Ama kontrolEt, bu işaretçiyi error tipinde bir değişkene döndürdüğü anda,
arayüz değeri iki parçalı hâle geliyor: dinamik tip (*AlanHatasi) ve dinamik değer (nil
işaretçi). Karşılaştırma hata == nil, arayüzün kendisinin boş olup olmadığına bakıyor;
dinamik tip dolu olduğu için arayüz de dolu sayılıyor, taşıdığı işaretçi nil olsa bile.
HT6. Bu, konunun ortak ölçütünün hata değerlerindeki en tuhaf karşılığı: on altı bağın
sekizi değerin içinde taşınıyordu ve bu satır tam olarak o sekizin neden bedelsiz olmadığını
gösteriyor. Bağın bozulduğu yer ne derleme, ne çağrı anı — yalnız değere == nil diye
sorulduğunda ortaya çıkıyor. Pratik sonucu açık: bir işlev error döndürecekse imzasında somut
bir işaretçi tipi değil, doğrudan error arayüzünü kullanmalı; aksi hâlde dogrula gibi bir
işlevin nil dönüşü, çağıranın elinde dolu bir hataya dönüşebiliyor.
Bu tuzağın sinsiliği, hatanın gizli kalmasında değil, tam tersinde: hata != nil denetimi
gayet dürüst bir biçimde çalışıyor ve doğru yanıtı veriyor — arayüz gerçekten dolu, çünkü
dinamik tipi taşıyor. Yanılan, kodu yazan kişinin beklentisi: dogrulanın nil döndürdüğü her
durumda kontrolEtin de nil döndüreceğini varsaymak. Bu varsayım, işaretçinin kendisiyle
işaretçiyi taşıyan arayüzün aynı şey olduğunu düşünmekten kaynaklanıyor; hâlbuki üçüncü satırın
gösterdiği gibi ikisi farklı sorulara yanıt veriyor.
Bu ders boyunca ölçülen dört gözlem tek bir çerçeveye oturuyor. Örtük karşılama bağı kaynakta
hiç yazılmıyor ve bunun yerini iki farklı mekanizma dolduruyor: derleyicinin bir atama satırında
denetlediği yöntem kümesi uyumu (kullanım yeri), ve arayüz değerinin kendi içinde taşıdığı
dinamik tip ile dinamik değer (değer). Bu ikisi birbirinden bağımsız değil — kullanım yerinde
kurulan bağ, sonradan değerin içinde taşınan çiftin neyi taşıyacağını belirliyor. nil
tuzağı da tam bu geçişte doğuyor: uyum bir kere kurulduktan sonra, taşınan değerin kendisi
nil olsa bile arayüz artık dolu.
Özet
error, tek yöntemli sıradan bir arayüzdür (Error() string); bir tipin onu karşıladığı kaynağın hiçbir yerinde yazmaz, yalnız yöntemin varlığıyla kurulur.- Değer alıcılı bir hata yapısal karşılaştırılır (alanlar eşitse değer eşittir); işaretçi alıcılı bir hata kimlik karşılaştırılır (adresler eşit değilse değer eşit değildir).
%Tile okunan dinamik tip, bir hata değerinin arayüzü hangi somut tiple karşıladığını gösterir; bu bilgi kaynakta değil, değerin kendisinde durur.- Somut bir işaretçi tipi
nilolsa bile, o işaretçi birerrordeğişkenine konduğunda arayüz değeri dolu sayılır — dinamik tip taşındığı için karşılaştırmafalsedöner. - Bu davranış,
errordöndürecek işlevlerin imzasında somut bir tip değil doğrudanerrorarayüzünü kullanmasının gerekçesidir. - Bağ iki yerde kuruluyor, hiçbirinde bildirimde değil: yöntem kümesi uyumu derleyicinin atama satırında denetlediği kullanım yeri, dinamik tip ile dinamik değerin çifti ise değerin kendisi.
Sonraki Adım
Bu derste ölçülen iki hata (BicimHatasi, ErisimHatasi) birer birer yazıldı ve her çağrıda
yeniden üretildi; hiçbiri paket düzeyinde tek bir değer olarak paylaşılmadı. Ama bir çağıran,
belirli bir hata türünü tanımak istediğinde — “bu, tam olarak şu hata mı?” diye sormak
istediğinde — hatanın metnine değil kimliğine güvenmesi gerekiyor. Bu derste ölçülen değer
alıcılı ve işaretçi alıcılı hataların karşılaştırma farkı, tam olarak bu sorunun zeminini
hazırlıyor: bir kimliği güvenilir kılmak için karşılaştırmanın yapısal değil kimlik tabanlı
olması gerekiyor. Nasıl ki bu derste error arayüzünü karşılayan tipin hiç error sözcüğünü anmadığı ölçüldüyse,
sıradaki ders bu kimliğin
nerede durduğunu ölçüyor: paket düzeyinde bir kez tanımlanan bir hata değeri, aynı metni taşıyan
başka bir hatadan neden hâlâ ayırt edilebiliyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.