İçeriğe geç
academia.sh

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).
  • %T ile 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 nil olsa bile, o işaretçi bir error değişkenine konduğunda arayüz değeri dolu sayılır — dinamik tip taşındığı için karşılaştırma false döner.
  • Bu davranış, error döndürecek işlevlerin imzasında somut bir tip değil doğrudan error arayü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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat