TG Telegram Group & Channel
Go Update | United States America (US)
Create: Update:

🆒 Fixing For Loops in Go 1.22

Рассказывая про счастливое далекое будущее, я часто забываю рассказать про хорошее (почти) настоящее. Но перед тем как начать, я задам вопрос: а что выведет следующая программа?


package main

import "fmt"

func main() {
var slc []fmt.Stringer

for _, s := range []st{{"Hello "}, {"World"}, {"!"}} {
slc = append(slc, s.toStringer())
}

fmt.Print(slc)
}

type st struct{ str string }

func (s *st) String() string { return s.str }

func (s *st) toStringer() fmt.Stringer { return s }


Те из вас кто давно знаком с языком скорее всего подозревают что здесь есть подвох, но вероятно сходу заметить его не смогут. Новички же ждут примерно такой вывод:


[Hello World !]


Пробуем запустить и видим совсем другой ответ: https://go.dev/play/p/5m7YTUeNjfl Возникает вопрос: почему?

В Go внутри цикла, на каждой итерации, испольузется одна и та же переменная. Фактически компилятор переписывает наш код вот в такую конструкцию:


temp := []st{{"Hello"}, {"World"}, {"!"}}

if len(temp) > 0 {
var s st
for i := 0; i < len(temp); i++ {
s = temp[i]
slc = append(slc, s.toStringer())
}
}


Однако это еще не все. Обратите внимание, что toStringer свою структуру получает по указателю. Для нас это происходит неявно, из-за того как работает вызов методов в Go. Дальше он конвертирует себя-по-указателю в тип fmt.Stringer и спокойно возвращает для вставки в слайс.

Проблема тут в том, что мы берем указатель на одну и ту же переменную все три итерации и каждый раз добавляем ее в слайс. И если во втором примере, это еще более менее очевидно, то в первом нужно знать о том, что циклы в Go создают переменную не на итерацию, а на весь цикл сразу. При этом Go тут не одинок - в JS и C# изначально было так же и создатели, впоследствии, решали ровно те же проблемы. В свое время (2012ый год ЕМНИП) Расс Кокс был уверен, что такую ошибку он никогда не допустит. Но сейчас, более десяти лет спустя, он был вынужден признать, что при всем своем опыте и знании Go, он совершил не один десяток подобных ошибок.

Поэтому начиная с версии Go 1.22 переменная цикла будет привязана к конкретной итерации, а не к телу цикла. Это значит, что компилятор будет трансформировать наш код немного иначе:

temp := []st{{"Hello"}, {"World"}, {"!"}}

if len(temp) > 0 {
for i := 0; i < len(temp); i++ {
var v st
v = temp[i]
slc = append(slc, v.toStringer())
}
}


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

Однако это все-же ломающее изменение, поэтому включено оно будет только в проекте где go.mod ставит версию go 1.22.0 и выше и только для самого модуля (модули которые вы импортируете продолжат работать по старому если сами не перейдут на go 1.22).

А попробовать можно уже сейчас:

- На playground через управляющий комментарий (работает только там) либо через Go dev branch. https://go.dev/play/p/QjEKVbvX59-
- Локально, если стоит переменная окружения GOEXPERIMENT=loopvar и у вас Go >= 1.21.0

🆒 Fixing For Loops in Go 1.22

Рассказывая про счастливое далекое будущее, я часто забываю рассказать про хорошее (почти) настоящее. Но перед тем как начать, я задам вопрос: а что выведет следующая программа?


package main

import "fmt"

func main() {
var slc []fmt.Stringer

for _, s := range []st{{"Hello "}, {"World"}, {"!"}} {
slc = append(slc, s.toStringer())
}

fmt.Print(slc)
}

type st struct{ str string }

func (s *st) String() string { return s.str }

func (s *st) toStringer() fmt.Stringer { return s }


Те из вас кто давно знаком с языком скорее всего подозревают что здесь есть подвох, но вероятно сходу заметить его не смогут. Новички же ждут примерно такой вывод:


[Hello World !]


Пробуем запустить и видим совсем другой ответ: https://go.dev/play/p/5m7YTUeNjfl Возникает вопрос: почему?

В Go внутри цикла, на каждой итерации, испольузется одна и та же переменная. Фактически компилятор переписывает наш код вот в такую конструкцию:


temp := []st{{"Hello"}, {"World"}, {"!"}}

if len(temp) > 0 {
var s st
for i := 0; i < len(temp); i++ {
s = temp[i]
slc = append(slc, s.toStringer())
}
}


Однако это еще не все. Обратите внимание, что toStringer свою структуру получает по указателю. Для нас это происходит неявно, из-за того как работает вызов методов в Go. Дальше он конвертирует себя-по-указателю в тип fmt.Stringer и спокойно возвращает для вставки в слайс.

Проблема тут в том, что мы берем указатель на одну и ту же переменную все три итерации и каждый раз добавляем ее в слайс. И если во втором примере, это еще более менее очевидно, то в первом нужно знать о том, что циклы в Go создают переменную не на итерацию, а на весь цикл сразу. При этом Go тут не одинок - в JS и C# изначально было так же и создатели, впоследствии, решали ровно те же проблемы. В свое время (2012ый год ЕМНИП) Расс Кокс был уверен, что такую ошибку он никогда не допустит. Но сейчас, более десяти лет спустя, он был вынужден признать, что при всем своем опыте и знании Go, он совершил не один десяток подобных ошибок.

Поэтому начиная с версии Go 1.22 переменная цикла будет привязана к конкретной итерации, а не к телу цикла. Это значит, что компилятор будет трансформировать наш код немного иначе:

temp := []st{{"Hello"}, {"World"}, {"!"}}

if len(temp) > 0 {
for i := 0; i < len(temp); i++ {
var v st
v = temp[i]
slc = append(slc, v.toStringer())
}
}


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

Однако это все-же ломающее изменение, поэтому включено оно будет только в проекте где go.mod ставит версию go 1.22.0 и выше и только для самого модуля (модули которые вы импортируете продолжат работать по старому если сами не перейдут на go 1.22).

А попробовать можно уже сейчас:

- На playground через управляющий комментарий (работает только там) либо через Go dev branch. https://go.dev/play/p/QjEKVbvX59-
- Локально, если стоит переменная окружения GOEXPERIMENT=loopvar и у вас Go >= 1.21.0
33👍11🔥4


>>Click here to continue<<

Go Update






Share with your best friend
VIEW MORE

United States America Popular Telegram Group (US)