Powrót do strony głównej

Pułapki Go: slice'y kanały nil

Artykuł analizuje nieoczekiwane zachowanie slice'ów, kanałów, stringów i nil w Go. Przykłady kodu z wyjaśnieniem wewnętrznej implementacji. Materiał dla middle/senior deweloperów.

Sekrety Go: dlaczego slice'y i kanały zawodzą
Advertisement 728x90

Podwodne kamienie w Go: slajsy, kanały i nil – szczegółowa analiza

Slajsy w Go wydają się proste, ale do przewidywalnego działania wymagają zrozumienia ich wewnętrznego mechanizmu. Odwołują się do podstawowego tablicy, co prowadzi do nieoczekiwanych modyfikacji i wycieków pamięci.

Rozważmy podstawowy przykład ponownego wykorzystania tablicy:

a := []int{1, 2, 3, 4}
b := a[1:3] // b = [2, 3]
b[0] = 99
fmt.Println(a)

Wynik: [1 99 3 4]. Modyfikacja w b wpływa na tablicę a.

Google AdInline article slot

Dodajmy append do a:

a := []int{1, 2, 3, 4}
b := a[1:3]
a = append(a, 5)
b[0] = 99 
fmt.Println(a)

Wynik: [1 2 3 4 5]. append przydzielił nową tablicę, więc zmiana b nie miała wpływu na oryginał.

Subtelności append i pojemności

func main() {
	a := []int{1, 2, 3, 4}
	_ = append(a[:3], 5)
	fmt.Println(a)
}

Wynik: [1 2 3 5]. Pojemność wystarczyła, tablica nie została przepisana.

Google AdInline article slot

Z jawnym określeniem pojemności:

func main() {
	a := []int{1, 2, 3, 4}
	_ = append(a[:3:3], 5)
	fmt.Println(a)
}

Wynik: [1 2 3 4]. Ograniczenie pojemności spowodowało przepisanie tablicy.

Rozszerzanie slajsu poza długość:

Google AdInline article slot
func main() {
	s := []int{1, 2, 3, 4, 5}[1:3]
	fmt.Println(s)
	extendedSlice := s[:4]
	fmt.Println(extendedSlice)
}

Wynik:

[2 3]

[2 3 4 5]

Wycieki pamięci i przekazywanie slajsów

Mały slajs od dużej tablicy utrzymuje cały bufor:

bigArray := make([]int, 1e6)
smallSlice := bigArray[:10]

Przekazywanie przez wartość modyfikuje tablicę pod spodem, ale nie zmienia pojemności:

func modifySlice(s []int) {
	s[0] = 99
	s = append(s, 100)
}

func main() {
	s := []int{1, 2, 3}
	modifySlice(s)
	fmt.Println(s)
}

Wynik: [99 2 3].

Pętla z odnośnikami do zmienianego elementu:

func main() {
	s := []int{}
	refs := []*int{}

	for i := 0; i < 5; i++ {
		s = append(s, i)
		refs = append(refs, &s[0])
	}

	*refs[4] = 4
	*refs[0] = 99999
	fmt.Println(s)
}

Wynik: [4 1 2 3 4].

Niespójność nil dla slajsów i map

var s []int
fmt.Println(len(s)) // 0
s = append(s, 1)

var m map[string]string
fmt.Println(len(m)) // 0
m["key"] = "value" // panic

Slajsy z nil działają, mapa – nie.

Ciągi znaków jako bajty

Ciągi znaków przechowują bajty:

func main() {
	str := "å"
	fmt.Println(str[1]) // 165
}
func main() {
	str := "Trzy"
	fmt.Println(len(str)) // 6
}

Przesłanianie predeclared identifiers

func main() {
	true := false
	uint := "bob"
	string := 0
	fmt.Printf("%v, %v, %v", true, uint, string)
}

Wynik: false, bob, 0.

Kanały: odczyt i zamykanie

Odczyt wymaga sprawdzenia stanu:

ch := getCountChannel[int]()

if v, ok := <-ch; ok {
	fmt.Println(v)
} else {
	fmt.Println("kanał zamknięty")
}

Ustawienie kanału na nil dezaktywuje gałąź select:

var in <-chan int = ch
if paused {
	in = nil
}

select {
case v := <-in:
	fmt.Println("otrzymano", v)
case <-ctx.Done():
	return
}

Po close() z kanału czytane są wartości domyślne:

func main() {
	ch := make(chan int, 1)
	ch <- 0
	close(ch)

	fmt.Println(<-ch) // 0 true
	fmt.Println(<-ch) // 0 false
	fmt.Println(<-ch) // 0 false
}

Z kontrolą:

v, ok := <-ch
fmt.Println(v, ok)

Typizowany nil w interfejsach

type MyErr struct{}

func (MyErr) Error() string { return "boom" }

func f() error {
	var e *MyErr = nil
	return e
}

func main() {
	err := f()
	fmt.Println(err == nil) // false
}

Interfejs zawiera typ i wartość. Rozwiązanie:

func f() error {
	var e *MyErr = nil
	if e == nil {
		return nil
	}
	return e
}

Problemy z for range i wskaźnikami

vals := []int{1, 2, 3}
ptrs := []*int{}

for _, v := range vals {
	ptrs = append(ptrs, &v)
}

fmt.Println(*ptrs[0], *ptrs[1], *ptrs[2]) // 3 3 3

Zmienna v jest ponownie używana.

Co ważne:

  • Slajsy odnoszą się do wspólnej tablicy: zmiany są widoczne wszędzie.
  • append może przepisać pamięć przy przekroczeniu pojemności.
  • nil slajsy pozwalają na append, nil mapa – nie.
  • Ciągi znaków przechowują bajty, len() liczy bajty.
  • Interfejsy z typizowanym nil nie są równe nil.
  • for range tworzy jedną zmienną pętli.

— Editorial Team

Advertisement 728x90

Czytaj dalej