Проверки в коде
Разберем функцию, которая проверяет разрешения пользователя:
Оставим за скобками вопросики к типу
Первая проблема — нет проверки на nil. Вторая — отсутствие early return (он же guard clause). С nil, думаю, и так понятно, а вот на раннем возврате остановимся подробнее.
В реальной жизни проверки в функциях часто бывают более сложными, вроде таких:
Читать такое больно из-за лесенки ифов. Лучше переделать на ранний возврат, а «основной сценарий» сделать без отступов:
Теперь явно видно, что делает функция, и что в ней может пойти не так.
Применим к
Можно еще заменить цикл на
Главное — не забывайте проверять на nil и использовать ранний возврат.
Разберем функцию, которая проверяет разрешения пользователя:
// CanEdit returns true if the user can edit objects.
func CanEdit(user *User) bool {
if user.IsActive() {
perms := user.Permissions()
for _, perm := range perms {
if perm == PermWrite {
return true
}
}
return false
} else {
return false
}
}
Оставим за скобками вопросики к типу
User
— например, почему CanEdit
не сделан методом. Для простоты будем считать, что тип User
менять нельзя.Первая проблема — нет проверки на nil. Вторая — отсутствие early return (он же guard clause). С nil, думаю, и так понятно, а вот на раннем возврате остановимся подробнее.
В реальной жизни проверки в функциях часто бывают более сложными, вроде таких:
func Do() {
if cond_1 {
// do stuff
if cond_2 {
// do more stuff
if cond_3 {
// do even more stuff
}
}
}
}
Читать такое больно из-за лесенки ифов. Лучше переделать на ранний возврат, а «основной сценарий» сделать без отступов:
func Do() {
if !cond_1 {
return
}
// do stuff
if !cond_2 {
return
}
// do more stuff
if !cond_3 {
return
}
// do even more stuff
}
Теперь явно видно, что делает функция, и что в ней может пойти не так.
Применим к
CanEdit
:func CanEdit(user *User) bool {
if user == nil || !user.IsActive() {
return false
}
for _, perm := range user.Permissions() {
if perm == PermRead {
return true
}
}
return false
}
Можно еще заменить цикл на
slices.Contains
(хотя это уже частности):func CanEdit(user *User) bool {
if user == nil || !user.IsActive() {
return false
}
perms := user.Permissions()
return slices.Contains(perms, PermRead)
}
Главное — не забывайте проверять на nil и использовать ранний возврат.
Thank Go!
🗒 Узнаем больше о Go разработке Ребята из DevCrowd (а они же основатели классного подкаста Podlodka) проводят исследование Go-разработчиков: - Какие навыки для go-разработчиков самые важные - Какие инструменты используются в работе - Как попадают в…
🗒 Узнаем больше о Go разработке 2024
Второй год подряд ребята из DevCrowd проводят большое исследование Go-разработчиков:
- Что входит в обязанности и каких навыков не хватает
- Сколько в среднем зарабатывают в профессии в зависимости от грейда
- Какие инструменты, сервисы наиболее популярны
- Что читают, слушают и смотрят для профессионального развития.
Проходите опрос, делитесь своими мнениями и помогите сделать исследование максимально охватным. Организаторы обещают сравнить данные с прошлым годом и поделиться выводами публично уже в конце ноября.
Результаты опроса помогут вам сравнить свои ожидания с рыночными, построить план своего развития, и просто понять, что происходит с индустрией!
👉 Пройти опрос
👀 Посмотреть результаты прошлого года
Второй год подряд ребята из DevCrowd проводят большое исследование Go-разработчиков:
- Что входит в обязанности и каких навыков не хватает
- Сколько в среднем зарабатывают в профессии в зависимости от грейда
- Какие инструменты, сервисы наиболее популярны
- Что читают, слушают и смотрят для профессионального развития.
Проходите опрос, делитесь своими мнениями и помогите сделать исследование максимально охватным. Организаторы обещают сравнить данные с прошлым годом и поделиться выводами публично уже в конце ноября.
Результаты опроса помогут вам сравнить свои ожидания с рыночными, построить план своего развития, и просто понять, что происходит с индустрией!
👉 Пройти опрос
👀 Посмотреть результаты прошлого года
README для начинающих разработчиков
Нашел тут отличную книжку. Я бы ее выдавал всем начинающим разработчикам, а то и изучал отдельным предметом на старших курсах универа:
The Missing README
Это по сути такой очень подробный чеклист на 250 страниц по всяким хорошим практикам в разных областях разработки:
— Работа с кодом
— Проектирование и архитектура
— Управление зависимостями
— Тестирование
— Ревью кода
— Сборка, интеграция и деплой
— Решение инцидентов с прода
— Планирование и оценка
— Общение с менеджером
— Дальнейшая карьера
Можно использовать для самопроверки (что уже применяете), ну и чтобы определить слабые места для дальнейшего развития.
Есть перевод на русский, но он местами с потерей исходного смысла.
Нашел тут отличную книжку. Я бы ее выдавал всем начинающим разработчикам, а то и изучал отдельным предметом на старших курсах универа:
The Missing README
Это по сути такой очень подробный чеклист на 250 страниц по всяким хорошим практикам в разных областях разработки:
— Работа с кодом
— Проектирование и архитектура
— Управление зависимостями
— Тестирование
— Ревью кода
— Сборка, интеграция и деплой
— Решение инцидентов с прода
— Планирование и оценка
— Общение с менеджером
— Дальнейшая карьера
Можно использовать для самопроверки (что уже применяете), ну и чтобы определить слабые места для дальнейшего развития.
Есть перевод на русский, но он местами с потерей исходного смысла.
Курсы на Степике
На степике сегодня скидка 20% на все курсы по промо-коду
И раз такое дело, предлагаю в комментариях делиться ссылками на курсы, которые лично вам очень понравились. И нет, это не завуалированное предложение похвалить мои курсы 😁 Принимается любой личный опыт.
На степике сегодня скидка 20% на все курсы по промо-коду
STEPIKSALE20
. Так что если давно хотели что-нибудь подучить, сейчас хороший момент, чтобы начать.И раз такое дело, предлагаю в комментариях делиться ссылками на курсы, которые лично вам очень понравились. И нет, это не завуалированное предложение похвалить мои курсы 😁 Принимается любой личный опыт.
Или
Допустим, ваше приложение по умолчанию слушает на порту 8080. Порт можно изменить, задав переменную окружения PORT. Как бы реализовать это в коде?
Можно так:
А с функцией
Мелочь, но приятная.
Допустим, ваше приложение по умолчанию слушает на порту 8080. Порт можно изменить, задав переменную окружения PORT. Как бы реализовать это в коде?
Можно так:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
А с функцией
cmp.Or
можно компактнее:port := cmp.Or(os.Getenv("PORT"), "8080")
Or
принимает любое количество аргументов и возвращает первый ненулевой. Работает в 1.22+Мелочь, но приятная.
Хотите длинную серию постов о скорости работы алгоритмов (O-нотация) на котиках?
Final Results
63%
Хочу
16%
Хочу один мегадлинный пост
7%
Хочу без котиков
6%
Хочу только котиков
8%
Ничего не хочу
Скорость алгоритмов: O-нотация
Скорость работы алгоритмов принято оценивать в О-нотации: у медленного она «о», у более быстрого «ооо», у самого быстрого «ооооо ваще огонь».
Вру, конечно.
На самом деле, скорость оценивают в количестве действий, которое выполняет алгоритм. Например, если у вас дома 10 котов, и нужно определить самого увесистого, то придется взвесить каждого кота, то есть выполнить 10 операций.
Алгоритм, понятное дело, должен работать одинаковым образом что для 10 котиков, что для 100, что для 10000. Оценка же скорости нужна одна. Поэтому оценивают не конкретное количество операций, а количество операций относительно количества входных данных (котов в нашем случае).
Если у вас
В реальности действий всегда будет не
А что делать, если котики отказываются взвешиваться без рыбов? Тогда на каждого кота приходится 2 действия: выдать рыбку и взвесить, поэтому общее количество действий
Ровно так это работает и в коде:
Здесь на каждого кота выполняется до трех действий:
1. Выбрать очередного кота из среза
2. Сравнить вес кота с весом жирнейшего
3. (возможно) Обновить жирнейшего
То есть действий может быть до
Таким образом, в O-нотации любые константные факторы игнорируются. Есть и другие нюансы, но с ними разберемся дальше по ходу дела.
P.S. Я пока не уверен, что реально нужна серия про скорость алгоритмов — может, для всех это совсем очевидная тема. Посмотрим, как пойдет.
🐈
Скорость работы алгоритмов принято оценивать в О-нотации: у медленного она «о», у более быстрого «ооо», у самого быстрого «ооооо ваще огонь».
Вру, конечно.
На самом деле, скорость оценивают в количестве действий, которое выполняет алгоритм. Например, если у вас дома 10 котов, и нужно определить самого увесистого, то придется взвесить каждого кота, то есть выполнить 10 операций.
Алгоритм, понятное дело, должен работать одинаковым образом что для 10 котиков, что для 100, что для 10000. Оценка же скорости нужна одна. Поэтому оценивают не конкретное количество операций, а количество операций относительно количества входных данных (котов в нашем случае).
Если у вас
n
котов, то чтобы определить самого жирненького, придется выполнить n
взвешиваний. В таком случае говорят, что скорость работы алгоритма — O(n)
.В реальности действий всегда будет не
n
, а несколько больше. Например, сначала нужно откалибровать весы, а в конце убрать их обратно в шкаф. Получается уже n+2
действия. Но поскольку дополнительных действий константное количество, их в оценке не учитывают: O(n+2)
= O(n)
А что делать, если котики отказываются взвешиваться без рыбов? Тогда на каждого кота приходится 2 действия: выдать рыбку и взвесить, поэтому общее количество действий
2n
. Но фактор 2× тоже константный, поэтому и его в оценке не учитывают: O(2n)
= O(n)
Ровно так это работает и в коде:
var fattest Cat
for _, cat := range cats {
if cat.weight > fattest.weight {
fattest = cat
}
}
Здесь на каждого кота выполняется до трех действий:
1. Выбрать очередного кота из среза
2. Сравнить вес кота с весом жирнейшего
3. (возможно) Обновить жирнейшего
То есть действий может быть до
3n+1
(+1 — инициализация переменной fattest
). Но скорость работы алгоритма — O(n)
Таким образом, в O-нотации любые константные факторы игнорируются. Есть и другие нюансы, но с ними разберемся дальше по ходу дела.
P.S. Я пока не уверен, что реально нужна серия про скорость алгоритмов — может, для всех это совсем очевидная тема. Посмотрим, как пойдет.
🐈
Скорость алгоритмов: O(1)
O(1), так же известно как «константное время». Самый лучший вариант, скорость алгоритма не зависит от количествакотиков входных данных.
🐾 Пример
Вы — счастливый обладатель N котиков. Каждый котик знает, как его зовут. Если позвать «Феликс!», то прибежит только он, а остальным N-1 жопкам пофиг.
В жизни время выполнения O(1) обычно встречается при работе со срезами и картами.
Срезы
У среза известен адрес в памяти первого элемента и размер каждого элемента. Следовательно, адрес i-го элемента тоже известен
С добавлением и удалением элементов сложнее — они рано или поздно приводят к созданию нового массива под срезом — а это уже операция O(n). Но если аккуратно все посчитать, то получится, что в среднем добавление/удаление тоже константные.
Карты
Не вдаваясь в подробности скажу, что за картой тоже скрывается массив. Грубо говоря, когда вы пишете
Поэтому доступ к элементу карты по ключу — тоже O(1).
🐈
O(1), так же известно как «константное время». Самый лучший вариант, скорость алгоритма не зависит от количества
🐾 Пример
Вы — счастливый обладатель N котиков. Каждый котик знает, как его зовут. Если позвать «Феликс!», то прибежит только он, а остальным N-1 жопкам пофиг.
🐈 🐈 🐈 🐈 🐈
🐈 🐈⬛ 🐈 🐈 🐈
↓
мяу!
В жизни время выполнения O(1) обычно встречается при работе со срезами и картами.
Срезы
У среза известен адрес в памяти первого элемента и размер каждого элемента. Следовательно, адрес i-го элемента тоже известен
(first + i*size)
— а значит, доступ к i-му элементу выполняется за константное время. Изменение i-го элемента аналогично — тоже O(1).s := make([]int, 1e6)
i := rand.IntN(1e6)
s[i] = 42 // O(1)
fmt.Println(s[i]) // O(1)
С добавлением и удалением элементов сложнее — они рано или поздно приводят к созданию нового массива под срезом — а это уже операция O(n). Но если аккуратно все посчитать, то получится, что в среднем добавление/удаление тоже константные.
Карты
Не вдаваясь в подробности скажу, что за картой тоже скрывается массив. Грубо говоря, когда вы пишете
m["answer"] = 42
, то Go превращает "answer" в целое число (хеш строки), после чего считает по нему нужный индекс в массиве. По этому индексу и находится значение 42.d := map[string]int{}
d["answer"] = 42 // O(1)
fmt.Println(d["answer"]) // O(1)
Поэтому доступ к элементу карты по ключу — тоже O(1).
🐈
Заглушить логи
С появлением пакета log/slog в Go 1.21 наконец-то стало возможно нормально вести журналы без использования внешних библиотек.
Если еще не пробовали, то рекомендую простенький туториал и доку, она понятная и с примерами.
Но вообще сказать я хотел не о самом
Сделать это несложно — просто используйте
Удобно!
С появлением пакета log/slog в Go 1.21 наконец-то стало возможно нормально вести журналы без использования внешних библиотек.
Если еще не пробовали, то рекомендую простенький туториал и доку, она понятная и с примерами.
log := slog.New(
slog.NewTextHandler(os.Stdout, nil),
)
log.Info("operation", "count", 3, "took", 50*time.Millisecond)
time=2024-12-09T10:20:47.660+00:00 level=INFO msg=operation count=3 took=50ms
Но вообще сказать я хотел не о самом
slog
, а о том, как в нем заглушить логи (например, для тестов или бенчмарков).Сделать это несложно — просто используйте
io.Discard
в качестве приемника для slog.TextHandler
:log := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
log.Info("Prints nothing")
Удобно!
Нас ждет sync/v2
Один из core-разработчиков Go внес предложение добавить вторую версию пакета sync.
В ней будут wait-группы, мьютексы и условные переменные (как в v1), плюс generic-версии Map и Pool.
На мой взгляд, v2-версии пакетов стдлибы это большое зло, потому что они по сути умножают размер интерфейса пакета в два раза. Надо знать и уметь работать как с v1, так и v2, да еще чего доброго в каких-то кодовых базах будут использоваться обе версии.
Но когда я озвучил свои сомнения, то получил ответ, из которого однозначно следует, что sync/v2 будет принят.
Так что радуйтесь, любители перемен!
Один из core-разработчиков Go внес предложение добавить вторую версию пакета sync.
В ней будут wait-группы, мьютексы и условные переменные (как в v1), плюс generic-версии Map и Pool.
На мой взгляд, v2-версии пакетов стдлибы это большое зло, потому что они по сути умножают размер интерфейса пакета в два раза. Надо знать и уметь работать как с v1, так и v2, да еще чего доброго в каких-то кодовых базах будут использоваться обе версии.
Но когда я озвучил свои сомнения, то получил ответ, из которого однозначно следует, что sync/v2 будет принят.
Так что радуйтесь, любители перемен!
Будущее стдлибы
По-хорошему, у каждого предложения на доработку (языка или стдлибы) должно быть обоснование. В нем описывают существующую проблему, варианты решения, и аргументируют, что предлагаемый вариант лучше альтернатив («ничего не делать» тоже альтернатива).
Забавно, что в качестве обоснования для создания sync/v2 исходно было указано буквально следующее:
> Мы успешно реализовали math/rand/v2, давайте теперь еще sync/v2 запилим.
Это, мягко говоря, не то что ожидаешь видеть для крупной доработки. Когда я указал на это Яну (автор предложения и один из основных участников команды разработки Go), он добавил подробностей.
Из них однозначно считывается, что если в пакете стдлибы используется any вместо дженериков — это достаточное основание сделать v2-пакет.
Как вы понимаете, почти все пакеты стдлибы сделаны до появления в языке дженериков. Соответственно, я делаю вывод, что нас ожидает еще много v2-пакетов.
Инжой.
По-хорошему, у каждого предложения на доработку (языка или стдлибы) должно быть обоснование. В нем описывают существующую проблему, варианты решения, и аргументируют, что предлагаемый вариант лучше альтернатив («ничего не делать» тоже альтернатива).
Забавно, что в качестве обоснования для создания sync/v2 исходно было указано буквально следующее:
> Мы успешно реализовали math/rand/v2, давайте теперь еще sync/v2 запилим.
Это, мягко говоря, не то что ожидаешь видеть для крупной доработки. Когда я указал на это Яну (автор предложения и один из основных участников команды разработки Go), он добавил подробностей.
Из них однозначно считывается, что если в пакете стдлибы используется any вместо дженериков — это достаточное основание сделать v2-пакет.
Как вы понимаете, почти все пакеты стдлибы сделаны до появления в языке дженериков. Соответственно, я делаю вывод, что нас ожидает еще много v2-пакетов.
Инжой.
Хотите серию заметок с разбором Go 1.24?
Final Results
63%
Хочу серию
30%
Хочу один длиннопост
1%
Не хочу
6%
Верните мне мой Go 1.17
Go 1.24: Псевдонимы generic-типов
Приближается февраль, а вместе с ним релиз Go 1.24. Ну а мы начинаем марафон новых фич и изменений!
Сначала напоминалка: псевдоним типа (type alias) в Go создает синоним для типа, не создавая новый тип.
Когда тип определен на основе другого типа, типы отличаются:
Когда тип объявлен как псевдоним другого типа, типы остаются одинаковыми:
Go 1.24 поддерживает generic-псевдонимы типов: псевдоним типа может быть параметризован, как и определенный тип.
Например, можно определить
Вполне возможно, что вы никогда не использовали обычные псевдонимы, и не будете использовать generic-псевдонимы. Но нужно же было пополнить вашу копилку бесполезных знаний :)
Завтра продолжим!
P.S. Если вы голосовали за длиннопост, он есть у меня.
Приближается февраль, а вместе с ним релиз Go 1.24. Ну а мы начинаем марафон новых фич и изменений!
Сначала напоминалка: псевдоним типа (type alias) в Go создает синоним для типа, не создавая новый тип.
Когда тип определен на основе другого типа, типы отличаются:
type ID int
var n int = 10
var id ID = 10
id = ID(n)
fmt.Printf("id is %T\n", id)
id is main.ID
Когда тип объявлен как псевдоним другого типа, типы остаются одинаковыми:
type ID = int
var n int = 10
var id ID = 10
id = n // works fine
fmt.Printf("id is %T\n", id)
id is int
Go 1.24 поддерживает generic-псевдонимы типов: псевдоним типа может быть параметризован, как и определенный тип.
Например, можно определить
Set
как generic-псевдоним для map
с логическими значениями (не то чтобы это было сильно полезно, но):type Set[T comparable] = map[T]bool
set := Set[string]{"one": true, "two": true}
fmt.Println("'one' in set:", set["one"])
fmt.Println("'six' in set:", set["six"])
fmt.Printf("set is %T\n", set)
'one' in set: true
'six' in set: false
Set is map[string]bool
Вполне возможно, что вы никогда не использовали обычные псевдонимы, и не будете использовать generic-псевдонимы. Но нужно же было пополнить вашу копилку бесполезных знаний :)
Завтра продолжим!
P.S. Если вы голосовали за длиннопост, он есть у меня.
Слабые указатели
Если вы сильный программист, то не читайте дальше
Слабый указатель (пакет weak) ссылается на объект, как обычный указатель. Но в отличие от обычного указателя, слабый указатель не способен удержать объект в памяти. Если на объект ссылаются только слабые указатели, сборщик мусора может освободить занимаемую им память.
Предположим, у нас есть тип blob (реализация скрыта для краткости):
И указатель на блоб размером в 1000 КБ:
Мы можем создать слабый указатель (
Обычный указатель не позволит сборщику мусора освободить занятую объектом память:
Слабый указатель же разрешает сборщику мусора освободить память:
Как видите,
Пр этом нет гарантии, что nil вернется сразу после того, как объект перестал использоваться (или в любое другое время позже). Рантайм сам решает, когда освобождать память, и освобождать ли вообще.
Слабые указатели могут пригодиться для реализации кэша больших объектов. Они гарантируют, что объект не будет оставаться в памяти только потому, что он находится в кэше.
Мы еще поговорим об этом в следующий раз.
Слабый указатель (пакет weak) ссылается на объект, как обычный указатель. Но в отличие от обычного указателя, слабый указатель не способен удержать объект в памяти. Если на объект ссылаются только слабые указатели, сборщик мусора может освободить занимаемую им память.
Предположим, у нас есть тип blob (реализация скрыта для краткости):
// Blob is a large byte slice.
type Blob []byte
И указатель на блоб размером в 1000 КБ:
b := newBlob(1000)
Мы можем создать слабый указатель (
weak.Pointer
) из обычного с помощью weak.Make
. А получить доступ к оригинальному указателю поможет Pointer.Value
:wb := weak.Make(newBlob(1000))
fmt.Println(wb.Value())
// Blob(1000 KB)
Обычный указатель не позволит сборщику мусора освободить занятую объектом память:
b := newBlob(1000)
fmt.Println("before GC =", b)
runtime.GC()
fmt.Println("after GC =", b)
before GC = Blob(1000 KB)
after GC = Blob(1000 KB)
Слабый указатель же разрешает сборщику мусора освободить память:
wb := weak.Make(newBlob(1000))
fmt.Println("before GC =", wb.Value())
runtime.GC()
fmt.Println("after GC =", wb.Value())
before GC = Blob(1000 KB)
after GC = <nil>
Как видите,
Pointer.Value
возвращает nil, если сборщик мусора уже освободил значение по указателю. Пр этом нет гарантии, что nil вернется сразу после того, как объект перестал использоваться (или в любое другое время позже). Рантайм сам решает, когда освобождать память, и освобождать ли вообще.
Слабые указатели могут пригодиться для реализации кэша больших объектов. Они гарантируют, что объект не будет оставаться в памяти только потому, что он находится в кэше.
Мы еще поговорим об этом в следующий раз.
Go 1.24: Улучшенная очистка
Это последняя заметка с жестью, дальше пойдут более человеколюбивые фичи, чесслово :)
Помните наш блоб?
Что если мы хотим запустить некоторую функцию очистки (cleanup function), когда объект будет собран сборщиком мусора?
Раньше для этого мы бы вызывали
AddCleanup прикрепляет функцию очистки к объекту. Она выполняется после того как объект становится недоступен (на него больше никто не ссылается).
Функция очистки выполняется в отдельной горутине — она последовательно обрабатывает все вызовы очистки в рамках программы. К одному и тому же указателю можно прикрепить несколько функций очистки.
Обратите внимание, что функция очистки не обязательно выполняется сразу после того, как объект стал недоступен; она может выполниться в любое время в будущем.
Более полный пример, который демонстрирует совместную работу слабых указателей и AddCleanup — в песочнице.
Это последняя заметка с жестью, дальше пойдут более человеколюбивые фичи, чесслово :)
Помните наш блоб?
b := newBlob(1000)
fmt.Printf("b=%v, type=%T\n", b, b)
b=Blob(1000 KB), type=*main.Blob
Что если мы хотим запустить некоторую функцию очистки (cleanup function), когда объект будет собран сборщиком мусора?
Раньше для этого мы бы вызывали
runtime.SetFinalizer
, который сложно использовать. Теперь есть его улучшенная версия — runtime.AddCleanup:func main() {
b := newBlob(1000)
now := time.Now()
// Регистрируем функцию, которую рантайм
// вызовет после сборки памяти объекта b.
runtime.AddCleanup(b, cleanup, now)
time.Sleep(10 * time.Millisecond)
b = nil
runtime.GC()
time.Sleep(10 * time.Millisecond)
}
func cleanup(created time.Time) {
fmt.Printf(
"object is cleaned up! lifetime = %dms\n",
time.Since(created)/time.Millisecond,
)
}
object is cleaned up! lifetime = 10ms
AddCleanup прикрепляет функцию очистки к объекту. Она выполняется после того как объект становится недоступен (на него больше никто не ссылается).
Функция очистки выполняется в отдельной горутине — она последовательно обрабатывает все вызовы очистки в рамках программы. К одному и тому же указателю можно прикрепить несколько функций очистки.
Обратите внимание, что функция очистки не обязательно выполняется сразу после того, как объект стал недоступен; она может выполниться в любое время в будущем.
Более полный пример, который демонстрирует совместную работу слабых указателей и AddCleanup — в песочнице.
Go 1.24: Швейцарские таблицы 🇨🇭
Спустя много лет команда Go решила изменить реализацию
Теперь она основана на SwissTable и предлагает несколько оптимизаций (со слов разработчиков, лично не проверял):
— Чтение и запись в больших картах (>1024 записей) быстрее на ~30%.
— Запись в аллоцированных картах (с установленной емкостью) быстрее на ~35%.
— Итерация в целом быстрее на ~10%, для карт с низкой наполненностью (большая емкость, мало записей) на ~60%.
Вернуться к старой реализации можно через переменную окружения
Если интересно, вот исходники.
Спустя много лет команда Go решила изменить реализацию
map
!Теперь она основана на SwissTable и предлагает несколько оптимизаций (со слов разработчиков, лично не проверял):
— Чтение и запись в больших картах (>1024 записей) быстрее на ~30%.
— Запись в аллоцированных картах (с установленной емкостью) быстрее на ~35%.
— Итерация в целом быстрее на ~10%, для карт с низкой наполненностью (большая емкость, мало записей) на ~60%.
Вернуться к старой реализации можно через переменную окружения
GOEXPERIMENT=noswissmap
при сборке (надо сказать, что не все остались довольны новыми картами).Если интересно, вот исходники.
Способна ли карта в Go скукожиться (освободить память при удалении элементов)?
Final Results
29%
Конечно да
41%
Конечно нет
31%
Не знаю 😐
Go 1.24: Конкурентно-безопасная карта
Лирическое отступление. Обычная карта в Go никогда не скукоживается, только растетпока не поглотит вселенную. GC не освобождает память, занятую самой картой, даже если удалять из нее элементы. Не изменилось это и в новой «швейцарской» карте в Go 1.24.
Но есть в Go еще одна карта, конкурентно-безопасная (sync.Map). И по странному стечению обстоятельств, в Go 1.24 у нее тоже новая реализация! Теперь она основана на concurrent hash-trie (помесь хэш-таблицы и префиксного дерева) и работает быстрее, особенно при модификациях карты.
Кроме того, новая sync.Map лучше освобождает память, чем предыдущая. Та тоже умела это делать, но там использовалась «поколенческая» модель, и память собиралась с запаздыванем. В новой никаких поколений нет, и память освобождается по мере удаления элементов.
Исходно новую расчудесную карту сделали для пакета unique в Go 1.23 — там как раз нужен был конкурентно-безопасный кэш. А теперь заметили, что и для пакета sync новая реализация отлично подходит. В результате sync.Map теперь по сути фасад к HashTrieMap.
Если вы страшный ретроград, вернуться к старой sync.Map можно через переменную
Карточный релиз какой-то получается!
Лирическое отступление. Обычная карта в Go никогда не скукоживается, только растет
Но есть в Go еще одна карта, конкурентно-безопасная (sync.Map). И по странному стечению обстоятельств, в Go 1.24 у нее тоже новая реализация! Теперь она основана на concurrent hash-trie (помесь хэш-таблицы и префиксного дерева) и работает быстрее, особенно при модификациях карты.
Кроме того, новая sync.Map лучше освобождает память, чем предыдущая. Та тоже умела это делать, но там использовалась «поколенческая» модель, и память собиралась с запаздыванем. В новой никаких поколений нет, и память освобождается по мере удаления элементов.
Исходно новую расчудесную карту сделали для пакета unique в Go 1.23 — там как раз нужен был конкурентно-безопасный кэш. А теперь заметили, что и для пакета sync новая реализация отлично подходит. В результате sync.Map теперь по сути фасад к HashTrieMap.
Если вы страшный ретроград, вернуться к старой sync.Map можно через переменную
GOEXPERIMENT=nosynchashtriemap
при сборке.Карточный релиз какой-то получается!
Скукоживание карт в Go
Вижу, не все могут поверить в коварство гошной карты, котораяделает кусь не отдает память.
Характерный комментарий:
Если есть сервер на го с сессиями которые реализованы в виде мапы, то даже после отключения клиентов и удаления ключей из нее память не будет освобождаться?🤌
Штош. Давайте разбираться.
Вот наш клиент с идентификатором и телом в 40 байт:
Создаем карту, добавляем 10К клиентов:
Размер кучи вырос до 1100 KB. Удаляем все записи из карты:
Ни байтика не отдала, зараза!
Попробуем хранить указатели вместо значений:
Почему часть памяти освободилась?
Здесь в памяти хранятся только указатели на клиентов, а сами значения (48B каждое) хранятся вне карты. Поэтому клиентов GC спокойно спокойно освобождает (ссылок-то на них больше нет), а вот внутренние структуры карты по-прежнему занимают память.
Напоследок предположим, что вместо легкого клиента у настолстенький боди-позитивный с телом на 1024B:
Что за ерунда? Мы же используем значения, а не указатели, почему память освободилась?
Если значения в карте достаточно большие (больше 128B) Go автоматически хранит в карте не сами значения, а указатели на них. Поэтому после GC занятая клиентами память освободилась, и осталась только занятая самой картой память размером 400KB.
песочница (можете поменять версию на dev, и убедиться, что в Go 1.24 ничего не изменилось)
Такие дела.
Вижу, не все могут поверить в коварство гошной карты, которая
Характерный комментарий:
Если есть сервер на го с сессиями которые реализованы в виде мапы, то даже после отключения клиентов и удаления ключей из нее память не будет освобождаться?🤌
Штош. Давайте разбираться.
Вот наш клиент с идентификатором и телом в 40 байт:
type Client struct {
id uint64
body [40]byte
}
Создаем карту, добавляем 10К клиентов:
printAlloc("initial")
m := make(map[int]Client)
for i := range 10000 {
m[i] = Client{id: uint64(i)}
}
runtime.GC()
printAlloc("after create")
initial: heap size = 109 KB
after create: heap size = 1110 KB
Размер кучи вырос до 1100 KB. Удаляем все записи из карты:
for i := range 10000 {
delete(m, i)
}
runtime.GC()
printAlloc("after delete")
after delete: heap size = 1110 KB
Ни байтика не отдала, зараза!
Попробуем хранить указатели вместо значений:
m := make(map[int]*Client)
for i := range 10000 {
m[i] = &Client{id: uint64(i)}
}
for i := range 10000 {
delete(m, i)
}
after create: heap size = 898 KB
after delete: heap size = 429 KB
Почему часть памяти освободилась?
Здесь в памяти хранятся только указатели на клиентов, а сами значения (48B каждое) хранятся вне карты. Поэтому клиентов GC спокойно спокойно освобождает (ссылок-то на них больше нет), а вот внутренние структуры карты по-прежнему занимают память.
Напоследок предположим, что вместо легкого клиента у нас
type Client struct {
id uint64
body [1024]byte
}
m := make(map[int]Client)
for i := range 10000 {
m[i] = Client{id: uint64(i)}
}
for i := range 10000 {
delete(m, i)
}
after create: heap size = 11683 KB
after delete: heap size = 434 KB
Что за ерунда? Мы же используем значения, а не указатели, почему память освободилась?
Если значения в карте достаточно большие (больше 128B) Go автоматически хранит в карте не сами значения, а указатели на них. Поэтому после GC занятая клиентами память освободилась, и осталась только занятая самой картой память размером 400KB.
песочница (можете поменять версию на dev, и убедиться, что в Go 1.24 ничего не изменилось)
Такие дела.
Go 1.24: os.Root
Новый тип os.Root ограничивает операции с файловой системой определенной директорией.
Функция OpenRoot открывает директорию и возвращает Root:
Методы Root работают внутри директории и не позволяют использовать пути за ее пределами:
Методы Root поддерживают большинство операций с файловой системой, доступных в пакете os:
Поработав с Root, не забудьтеположить на место его закрыть:
На большинстве платформ создание Root открывает файловый дескриптор. Если директорию переместить пока Root открыт, методы будут корректно использовать новый каталог.
Новый тип os.Root ограничивает операции с файловой системой определенной директорией.
Функция OpenRoot открывает директорию и возвращает Root:
dir, err := os.OpenRoot("data")
fmt.Println(dir.Name(), err)
// data <nil>
Методы Root работают внутри директории и не позволяют использовать пути за ее пределами:
file, err := dir.Open("01.txt")
fmt.Println(file.Name(), err)
// data/01.txt <nil>
file, err = dir.Open("../main.txt")
fmt.Println(err)
// openat ../main.txt: path escapes from parent
Методы Root поддерживают большинство операций с файловой системой, доступных в пакете os:
file, err := dir.Create("new.txt")
stat, err := dir.Stat("02.txt")
err = dir.Remove("03.txt")
Поработав с Root, не забудьте
dir, err := os.OpenRoot(path)
defer dir.Close()
// do stuff
На большинстве платформ создание Root открывает файловый дескриптор. Если директорию переместить пока Root открыт, методы будут корректно использовать новый каталог.