V Go se nil != nil, někdy

3 Go, Tipy & triky

Go někdy vrátí false při porovnání proměnné s nil hodnotou a nil. Pro začátečníky to může být dosti matoucí. Ale má to své odůvodnění i benefity. Třeba volání metod nad nulovým objektem.

V Go se nil != nil, někdy

Article in English can be found on dev.to/arxeiss/in-go-nil-is-not-equal-to-nil-sometimes-jn8

Následující kód je pěkným příkladem, kdy proměnná val je nil ale porovnání ve funkci je vyhodnoceno jako false. Podobně pak i v případě readFile, který vrací error. Kód je možné spustit v Go Playgroundu a ověřit si tak, že opravdu nekecám.

func nilPrint(val interface{}) string {
	if val == nil {
		return "I got nil"
	}
	return fmt.Sprintf("I got %T type with value '%v'", val, val)
}

func readFile() error {
	var err *fs.PathError
	fmt.Println(err == nil)
	return err
}

func main() {
	// Prints: I got string type with value 'Some value'
	fmt.Println(nilPrint("Some value"))
	// Prints: I got nil
	fmt.Println(nilPrint(nil))
	// Prints: true
	// but then: I got *string type with value '<nil>'
	var val *string
	fmt.Println(val == nil) // Prints true
	fmt.Println(nilPrint(val))
fmt.Println("\nWith error interface") // Prints: true (inside readFile) // but then: false (in main method) err := readFile() fmt.Println(err == nil) }

Důvodem je interface

Program se tak chová proto, že funkce nilPrint očekává na vstupu interface{}, což značí prázdný interface, a tedy něco jako Any. Druhé funkce vrací error, což je také interface. Porovnání s nil pak vrací false, protože tato proměnná je sice nulová, ale má také datový typ. Pokud se porovnává proměnná známého typu s nil, tento problém nenastává.

Řešení pomocí reflexe

Oprava funkce, která vrací chybu je jednoduchá. Nedávat proměnné err konkrétní typ, ale ponechat typ error. Tedy změnit první řádek takto var err error. Ostatně proměnné, které obsahují chyby, by nikdy neměly mít konkrétní datový typ.

V druhém případě je to ale složitější. Z definice funkce je zřejmé, že je potřeba přijímat cokoli. Řešení tohoto problému už je pouze v použití reflexe. 

if val == nil || (reflect.ValueOf(val).Kind() == reflect.Ptr && reflect.ValueOf(val).IsNil()) {
	return "I got nil"
}

Volání metody na nulovém objektu

Nevím, jestli to přímo souvisí s výše popsaným problémem. Ale v jiných jazycích toto možné není, tak jsem se rozhodl to zde zmínit.

Go nemá klasické metody jako jiný OOP jazyk. V Go se to nazývá receivers, což se dá přeložit jako příjemce. A zde nastává také první velký rozdíl. Pokud je proměnná nějakého datového typu, ale zároveň její hodnota je nil, lze stále volat tyto receivery či metody. Kód níže tak neskončí s panic chybou, ale úplně v pohodě proběhne. Lze opět vyzkoušet na Go Playground.

type JSONParser struct {
	raw string
}

func (p *JSONParser) Length() int {
	if p == nil {
		return 0
	}
	return len(p.raw)
}

func main() {
	var parser *JSONParser
	fmt.Println(parser.Length()) // Prints: 0

	parser = &JSONParser{raw: `{"just": "show", "some_result": 255}`}
	fmt.Println(parser.Length()) // Prints: 36
}

Zkušenosti a postřehy s Go můžete sdílet se čtenáři v komentářích

K tomuto článku již není možné přidávat další komentáře

Komentáře

Tuto vlastnost Go radši nebudu komentovat :) Ale volat metody nad NULL adresou v C++ jde (samozřejmě je to UB :) ).

class X { void print() { cout << "x"; }};

int main() {
X* x = nullptr;
x->print();
}

Zajímavé, že to funguje i v C++ jsem netušil. Díky

Metody jsou běžné funkce, které akorát dostanou this jako skrytý parametr, takže pokud se v nich na this nešahá, tak teoreticky může být this cokoliv.
Ale samozřejmě "funguje" je silně v uvozovkách, protože to je UB, takže to může udělat cokoliv :)