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.
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.
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ść:
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.
appendmoże przepisać pamięć przy przekroczeniu pojemności.nilslajsy pozwalają naappend,nilmapa – nie.- Ciągi znaków przechowują bajty,
len()liczy bajty. - Interfejsy z typizowanym
nilnie są równenil. for rangetworzy jedną zmienną pętli.
— Editorial Team
Brak komentarzy.