В Go есть всеми знакомая конструкция для итерации по всем элементам слайса/массива:
for i, v := range slice {
// ...
}
i - текущий индекс элементаv - копия текущего элемента слайса (slice[i])И индекс, и значение мы можем опускать если не используем, с помощью
_. Но зачем разработчики языка Go дали нам 2 разные возможности итерации по слайсам? И какой способ в итоге лучше использовать?Рассмотрим пример:
s := []int{1, 2, 3}
for _, v := range s {
v++
}
fmt.Println(s)
Данный код выведет нам в консоль
[1, 2, 3]. Почему так? В переменную v кладется копия элемента слайса и v никак не ссылается на элемент. В итоге изменение переменной v не приводит к изменению элементов слайса.Обратный пример:
s := []int{1, 2, 3}
for i := range s {
s[i]++
}
fmt.Println(s)
Здесь уже программа нам выведет
[2, 3, 4], поскольку конструкция s[i] получается неявной ссылкой на элемент слайса и тут уже нет никакого копирования, мы применяем оператор инкремента (++) непосредственно к самому элементу.Ага, значит если нам надо менять содержимое элементов слайса, то тогда нужно использовать итерацию по индексу, а в остальных случаях по значению? - Не совсем так.
Допустим у нас элементы слайса не
int, а большая структура:type BigStruct struct { // size=1032 bytes
_ [1024]byte
Field int64
}
Попробуем проитерироваться по слайсу больших структур по индексу и по значению, сравнив оба подхода в бенчмарке:
func Benchmark(b *testing.B) {
const elems = 1000
s := make([]BigStruct, elems)
tmps := make([]int64, elems) // need to avoid compiler optimizations for loops
b.ResetTimer()
b.Run("iterate slice of structs by index", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for idx := range s {
tmps[idx] += s[idx].Field
}
}
})
b.Run("iterate slice of structs by value", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for idx, v := range s {
tmps[idx] += v.Field
}
}
})
}
Результат бенчмарка будет следующий:
Benchmark/iterate_slice_of_structs_by_index-10 1626810 735.2 ns/op 0 B/op 0 allocs/op
Benchmark/iterate_slice_of_structs_by_value-10 60226 19841 ns/op 0 B/op 0 allocs/op
Тут мы видим, что в данном случае итерация по индексу в РАЗЫ быстрее. Почему? Потому что Go при итерации по значению вынужден при каждой итерации копировать большую структуру в переменную. Чем больше объект, тем больше стоит его копирование. Если посмотреть такой код в профилировщике, то вы увидите, что работает
runtime.duffcopy. Если такое встретится у вас, смело переходите на итерацию по индексу.А если у нас слайс указателей?
s := make([]*BigStruct, elems)
Тогда при итерации по значению будет происходить только копирование указателя, что довольно дешево. В этом случае особой разницы в итерации по индексу или по значению нет.
В итоге для себя я сформировал следующие правила:
1. Если у нас слайс указателей - итерируюсь всегда по значению (если только нам не нужно проинициализировать пустой указатель).
2. Если у нас слайс структур - итерируюсь всегда по индексу (ни к чему нам лишние копирования).
3. Если у нас слайс чисел: нужно менять элементы - индекс, не нужно - значение.
Надеюсь эти правила помогут вам с code review 😉
#golang