Эти сложные map & slice в GO

от автора

В этой статье я хочу погрузиться в то, как работают некоторые структуры (далее ниже) в ГО. Хотя я и работаю с ГО уже 3й год, все равно есть вещи, в которые интересно погружаться. Хочу отметить, что я не буду погружаться прям сильно в реализацию того как устроены map и slice, скорее на столько, что бы понимать как они ведут себя и почему. Такое часто могут спрашивать на собеседованиях или это поможет писать более качественный и безопасный код.

Итак на сколько мы знаем (я надеюсь, что и вы читаете статью уже со знанием ГО) в ГО можно разделить типы переменных глобально на 2 группы

  • Value types — это простые типы такие как int, float, array, struct, string,…

  • Reference type — это сcылочные типы, такие как chan, map, slice

Для value type мы можем использовать ключевое слово new или литералы для создания

foo := "Some string" bar := new(int) // zero value is 0 + heap memory allocation

Для reference type мы должны использовать ключевое слово make для создания или литерал, иначе вы получите panic при попытки что-то сделать с nil

myMap := make(map[string]string) // myMap := map[string]string{} - literal for create a map myMap["foo"] = "foo" // all good here  var myBMap map[string]string myBMap["bar"] = "bar" // panic, don't do this!

Одна из интересных механик в ГО это как ведут себя переменные, которые мы прокидываем в функции. Для обычных типов (value type). Если мы передадим по значению и сделаем какие то действия с ней, то оригинальная переменная не поменяет свое значение, потому, что создается копия объекта в стеке функции (область памяти, которая очищается при завершения функции). Однако, когда мы передаем по ссылке, тогда любые манипуляции будут отражаться и на оригинальной переменной.

type User struct {   Name string }  func makeChangesWithVal(user User) {   user.Name = "Peter" }  func main() {   user := User{ Name: "Ivan" }   makeChangesWithVal(user)    fmt.Println(user.Name) // "Ivan" }
type User struct {   Name string }  func makeChangesWithVal(user *User) {   user.Name = "Peter" }  func main() {   user := User{ Name: "Ivan" }   makeChangesWithVal(user)    fmt.Println(user.Name) // "Peter" }

Однако ссылочные переменные (reference type) ведут себя по другому. Потому, что для их при создании выделяется память и создается дескриптор (специальная структура, которая содержит метаданные и ссылку на выделенную область памяти для мастер данных).

func DoSomeWithMap(myMap map[string]string) {   myMap["foo"] = "foo value" }  func main() {   fooMap := make(map[string]string)    DoSomeWithMap(fooMap)   fooKey, exists := fooMap["foo"]    fmt.Println(exists, fooKey) // true, "foo value" }

Ого! даже когда мы передаем «по значению» и добавляем новый ключ к map, мы видим изменения оригинальной переменной. Давайте посмотрим на еще один интересный пример с передачей «по значению»

func DoSomeWithMap(myMap map[string]string) {   myMap = make(map[string]string)   myMap["foo"] = "foo value" }  func main() {   fooMap := make(map[string]string)    DoSomeWithMap(fooMap)   fooKey, exists := fooMap["foo"] // fooKey is empty string here    fmt.Println(exists, fooKey) // false }

Интересно мы сделали ремейк (make) той же самой переменной в функции, однако она не поменялась. Но почему так ?

Как я писал выше когда мы создаем экземпляр класса ссылочных структур таких как map, slice, chan, данные кладутся в область памяти и создается дескриптор с ссылкой на эту область, и когда мы передаем эти переменные в функцию (по значению), то мы передаем дескриптор, при этом в функции создается новый дескриптор, но ссылка на область памяти остается той же(ссылается на туже область памяти) и когда мы меняем что-то то меняется именно данные по ссылке, при этом после завершении, дескриптор очищается(удаляется из памяти) но ссылка остается.

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

Для slice работает тоже со своей логикой

func DoSomeWithSlice(mySlice []string) {   mySlice[0] = "foo value" }  func main() {   fooSlice := make([]string, 3)   DoSomeWithSlice(fooSlice)    fmt.Println(fooSlice, cap(fooSlice), len(fooSlice)) // [foo value  ] 3 3 }
func DoSomeWithSlice(mySlice []string) {   mySlice = append(mySlice, "foo value") }  func main() {   fooSlice := make([]string, 3)   DoSomeWithSlice(fooSlice)    fmt.Println(fooSlice, cap(fooSlice), len(fooSlice)) // [  ] 3 3 }

Во втором случае нужно точно знать как работает append, что оставлю вам в качестве самостоятельного исследования.

Жду ваших комментариев и критики


ссылка на оригинал статьи https://habr.com/ru/articles/912272/