Создание сегмента при его объявлении

Мне всегда казалось, что сегменты не создаются при их объявлении, наверное из-за книги по которой я начинал изучать гоу «Head First. Изучаем Go » 2020 ISBN 978-5-4461-1395-8(страница 210) вот строки оттуда: введите сюда описание изображения

Но наткнулся на то, что всё работает и так:

package main
import "fmt"

func main() {
    var strArr []string
    fmt.Print(strArr)
    strArr = append(strArr,"qwerty")
    fmt.Print(strArr)
}

результат:

[][qwerty]

...Program finished with exit code 0
Press ENTER to exit console

Получается, что у меня устаревшая информация и книга была старого издания, либо я в чём-то заблуждаюсь. Исходя из приведённою мною примера, при объявлении сегмента, он всё-таки создаётся автоматически, так ли это?


Ответы (2 шт):

Автор решения: Vladimir Ignatenko

Немного поломал голову над тем что такое "сегмент". Я так понял это перевод slice. Вроде бы обычно переводят как "слайс".

Теперь по существу. Вот такой код

package main

import "fmt"

func main() {
    var a []string //объявление переменной, пустой слайс
    fmt.Println(a)
    a = make([]string, 7) //выделили память
    fmt.Println(a)
    a = append(a, "qwerty") //добавили строку
    fmt.Println(a)
}

Выдает вот такой результат

[]
[      ]
[       qwerty]

Т.е. при объявлении переменной она инициализируется пустым значением. Память имеет смысл выделять через make в том случае если есть планы работать именно с этим фрагментом. А append она, если по простому (и не вдаваться в тонкости реализации), берет два слайса, определяет сколько нужно памяти для того чтобы поместить туда данные из обоих и выделяет эту память, а потом копируюет данные.

Т.е. в вашем примере сначала был объявлен пустой слайс, а потом к нему присоединили данные. В общем-то все штатно - так оно и должно работать. А информацию в книге вы возможно неправильно поняли.

→ Ссылка
Автор решения: Pak Uula

Когда переменная объявляется без инициализатора, ей присваивается zero value. Для слайсов это nil. Ваш пример работает по той причине, что встроенные функции len, range и append корректно работают с nil-овым аргументом. len возвращает 0, range возвращает пустой итератор, а append аллоцирует память и размещает в ней переданные параметры функции. Важно отметить, что после того как вы передали в функцию append nil, append уже не вернёт nil, а вернёт слайс с аллоцированной памятью.

Пример: https://go.dev/play/p/c35hkN_vVbY

package main

import "fmt"

func IsNilSlice[T any](slice []T) bool { return slice == nil }

func main() {
    var zero []int
    fmt.Println(IsNilSlice(zero))
    empty := []int{}
    fmt.Println(IsNilSlice(empty))
}

Результат:

true
false

UPDATE

Про empty := []int{} и var empty []int; empty = make([]int,0)

В пакете reflect есть (пока что) структура SliceHeader, которая собственно, и есть слайс в рантайме:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

Она состоит из трёх полей: указатель на данные, длина слайса и размер куска памяти, на который указывает поле Data.

Посмотрим, как устрены пустые слайсы empty := []int{} и var empty []int; empty = make([]int,0):

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func SliceHeader[T any](slice []T) reflect.SliceHeader {
    hdrp := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
    return *hdrp
}

func main() {
    s1 := []int{}
    hdr1 := SliceHeader(s1)
    fmt.Printf("[]int{}\t\t: header: %#v\n", hdr1)
    var s2 []int
    s2 = make([]int, 0)
    hdr2 := SliceHeader(s2)
    fmt.Printf("make([]int, 0)\t: header: %#v\n", hdr2)
}

Результат: https://go.dev/play/p/8puJZ_wTFhJ

[]int{}     : header: reflect.SliceHeader{Data:0xc0000466e8, Len:0, Cap:0}
make([]int, 0)  : header: reflect.SliceHeader{Data:0xc000084ed0, Len:0, Cap:0}

В обоих случаях это какой-то указатель, нулевая длина и нулевая ёмкость. Указатель на участок памяти нулевой длины, конечно, выглядит странно, но может быть так надо для сборщика мусора.

После append память аллоцируется, указатель изменяется:

func main() {
    s1 := []int{}
    hdr1 := SliceHeader(s1)
    fmt.Printf("[]int{}\t\t\t: header: %#v\n", hdr1)
    var s2 []int
    s2 = make([]int, 0)
    hdr2 := SliceHeader(s2)
    fmt.Printf("make([]int, 0)\t\t: header: %#v\n", hdr2)

    s1 = append(s1, 1)
    fmt.Printf("[]int{} appended\t: header: %#v\n", SliceHeader(s1))

    s2 = append(s2, 1)
    fmt.Printf("make([]int, 0) appended\t: header: %#v\n", SliceHeader(s2))
}

Результат: https://go.dev/play/p/yoq-owhbb00

[]int{}         : header: reflect.SliceHeader{Data:0xc000046698, Len:0, Cap:0}
make([]int, 0)      : header: reflect.SliceHeader{Data:0xc000084e80, Len:0, Cap:0}
[]int{} appended    : header: reflect.SliceHeader{Data:0xc000012078, Len:1, Cap:1}
make([]int, 0) appended : header: reflect.SliceHeader{Data:0xc000012098, Len:1, Cap:1}

Оба варианта "пустого" слайса ведут себя одинаково. Поэтому можно считать, что это варианты инциализации эквивалентны.

→ Ссылка