Go и утечки памяти: почему GC не всегда спасает

В Go есть garbage collector, поэтому легко подумать: если объект больше не нужен, память сама освободится. Но GC освобождает только то, на что больше никто не ссылается. И вот здесь начинаются интересные случаи. 1. Маленький срез может удерживать гигабайт func findSequence(data []byte) []byte { for i := 0; i < len(data)-1; i++ { if data[i] == 0xFF && data[i+1] == 0xEE { return data[i+2 : i+12] } }

return nil } Допустим, data занимает 1 GB. Мы нашли в нём 10 байт и вернули их как slice. Кажется, что теперь приложение хранит только 10 байт. Но на самом деле эти 10 байт всё ещё ссылаются на исходный массив на 1 GB. Поэтому GC не может освободить весь массив. Если большая исходная область больше не нужна, иногда правильнее сделать копию: result := make([]byte, 10) copy(result, data[i+2:i+12])

return result Теперь большой массив можно освободить. То же самое относится к string: small := strings.Clone(big[i : i+10]) 2. data = data[:0] не освобождает память Ещё одна распространённая ловушка: data := make([]int, 1<<30)

// работа с большим массивом

data = data[:0] Длина стала 0. Но capacity осталась огромной, и исходный backing array всё ещё удерживается. Если большой массив больше не нужен: data = nil Или нужно создать новый небольшой срез и скопировать туда действительно нужные данные. 3. map после delete не обязательно становится маленьким Например: data := make(map[int][128]byte)

for i := 0; i < 1_000_000; i++ { data[i] = [128]byte{} }

for i := 0; i < 1_000_000; i++ { delete(data, i) } Логически map пустой. Но это не означает, что вся память, которую map успел занять, сразу вернулась системе. Если это был временный большой набор данных, а дальше нужен небольшой map, иногда проще создать новый: data = make(map[int][128]byte) Старый map больше никому не нужен → GC сможет его собрать. 4. Утечь может не только память, но и goroutine Например: func process(messages []string) { ch := make(chan string)

go func() { for message := range ch { fmt.Println(message) } }()

for _, message := range messages { ch <- message } } Здесь есть проблема: канал никто не закрывает. Горутина навсегда остаётся ждать следующий элемент. Это уже goroutine leak. Правильнее обозначить завершение работы: close(ch) Есть и обратная ситуация: resultCh := make(chan string)

for _, query := range queries { go func() { resultCh <- doQuery() }() }

return <-resultCh Мы забрали только один результат. Остальные goroutine могут навсегда зависнуть на отправке в resultCh, если больше никто не читает из канала. И вот что мне нравится в этой теме. Утечка памяти в Go — это не обязательно забытый free(). Очень часто проблема выглядит гораздо прозаичнее: маленький slice держит огромный backing array; пустой slice всё ещё держит большой массив; большой map продолжает занимать память после удаления элементов; маленькая строка удерживает большую исходную строку; goroutine навсегда зависла на канале; неправильная синхронизация оставила goroutine заблокированной. GC здесь работает правильно. Просто с точки зрения GC объект всё ещё достижим. Поэтому при проблемах с памятью в Go я бы смотрел не только на количество аллокаций.

Go и утечки памяти: почему GC не всегда спасает | Сетка — социальная сеть от hh.ru