
В этой статье я хочу погрузиться в то, как работают некоторые структуры (далее ниже) в ГО. Хотя я и работаю с ГО уже 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, что оставлю вам в качестве самостоятельного исследования.
Жду ваших комментариев и критики