2615
Здравый взгляд на язык программирования Go. Злой админ @nalgeon. Добрый админ @mikeberezin. Рекламы нет.
Или
Допустим, ваше приложение по умолчанию слушает на порту 8080. Порт можно изменить, задав переменную окружения PORT. Как бы реализовать это в коде?
Можно так:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}cmp.Or можно компактнее:port := cmp.Or(os.Getenv("PORT"), "8080")Or принимает любое количество аргументов и возвращает первый ненулевой. Работает в 1.22+
README для начинающих разработчиков
Нашел тут отличную книжку. Я бы ее выдавал всем начинающим разработчикам, а то и изучал отдельным предметом на старших курсах универа:
The Missing README
Это по сути такой очень подробный чеклист на 250 страниц по всяким хорошим практикам в разных областях разработки:
— Работа с кодом
— Проектирование и архитектура
— Управление зависимостями
— Тестирование
— Ревью кода
— Сборка, интеграция и деплой
— Решение инцидентов с прода
— Планирование и оценка
— Общение с менеджером
— Дальнейшая карьера
Можно использовать для самопроверки (что уже применяете), ну и чтобы определить слабые места для дальнейшего развития.
Есть перевод на русский, но он местами с потерей исходного смысла.
Проверки в коде
Разберем функцию, которая проверяет разрешения пользователя:
// 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 менять нельзя.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)
}
Кто канал создал, тот и закрывает
В Go есть одна эвристика, которую лучше не нарушать без веских причин: кто канал создал, тот и закрывает.
Поэтому мне больше по душе такая реализация канала завершения:
func work() <-chan struct{} {
done := make(chan struct{})
go func() {
// do work
time.Sleep(10 * time.Millisecond)
close(done)
}()
return done
}done := work()
<-done
chan struct{} на нужный тип вроде chan int, и готово.type Result[T any] struct {
Value T
Err error
}
func work() <-chan Result[int] {
out := make(chan Result[int], 1)
go func() {
// do work
time.Sleep(10 * time.Millisecond)
out <- Result[int]{Value: 42}
close(out)
}()
return out
}out := work()
result := <-out
fmt.Println(result)
// {42 <nil>}
Про печеньки
Давайте поговорим об http-куках. Если вы думаете, что это такие пары «ключ-значение», которые сервер и клиент гоняют туда-сюда в каждом запросе, то это верно лишь отчасти.
За годы развития веба куки обзавелись дополнительной атрибутикой. Вот как выглядит структура Cookie в Go 1.23:
— Name и Value — ключ и значение.
— Quoted — true, если значение передано в кавычках.
— Path — разрешает куку на страницах, которые начинаются с указанного пути.
— Domain — разрешает куку на указанном домене и всех его поддоменах.
— Expires — дата окончания годности куки.
— MaxAge — срок жизни куки в секундах.
— Secure — разрешает куку только по HTTPS.
— HttpOnly — закрывает доступ к куке из JavaScript.
— SameSite — разрешает или запрещает куку при кросс-доменных запросах.
— Partitioned — ограничивает доступ к third-party кукам.
Неслабо, да?
Начиная с версии Go 1.23, серверную куку можно распарсить из строки с помощью http.ParseSetCookie:
line := "session_id=abc123; SameSite=None; Secure; Partitioned; Path=/; Domain=example.com"
cookie, err := http.ParseSetCookie(line)
http.ParseCookie:line := "session_id=abc123; dnt=1; lang=en; lang=de"
cookies, err := http.ParseCookie(line)
Уникальные значения
Продолжаем разбирать нововведения в Go 1.23.
Новый пакет unique помогает сэкономить память, если обрабатываются неуникальные значения.
Рассмотрим пример. У нас есть генератор слов:
const nDistinct = 100
const wordLen = 40
generate := wordGen(nDistinct, wordLen)
fmt.Println(generate())
// nlfgseuif...
fmt.Println(generate())
// anixapidn...
fmt.Println(generate())
// czedtcbxa...
words = make([]string, nWords)
for i := range nWords {
words[i] = generate()
}
Memory used: 622 KB
unique.Handle, чтобы назначить дескриптор каждому уникальному слову, и будем хранить эти дескрипторы вместо самих слов:words = make([]unique.Handle[string], nWords)
for i := range nWords {
words[i] = unique.Make(generate())
}
Memory used: 95 KB
Make создает уникальный дескриптор для значения любого comparable-типа. Она возвращает ссылку на «каноническую» копию значения в виде объекта Handle.Handle равны только в том случае, если равны исходные значения. Сравнение двух Handle эффективно, потому что сводится к сравнению указателей.unique ведет глобальный конкурентно-безопасный кеш всех добавленных значений, гарантируя их уникальность и повторное использование.
Ча-ча-ча
(несерьезное) В чате спросили, разбирается ли на курсе тема канала каналов (chan chan). Пользуясь случаем, хочу вам его представить.
Встречайте: ча, чача, и ча-ча-ча:
cha := make(chan int, 1)
chacha := make(chan chan int, 1)
chachacha := make(chan chan chan int, 1)
cha <- 1
chacha <- cha
chachacha <- chacha
fmt.Printf("%#v\n", chachacha)
// (chan chan chan int)(0x14000102180)
Таймеры в Go 1.23
Тут прям детективная история приключилась. В Go есть таймер (тип Timer), а в нем — поле с каналом (Timer.C), в который таймер тикает спустя указанное время.
В коде стдлибы таймер создается так:
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
// ...
}
startTimer(&t.r)
return t
}time.After и Reset, от которых многие страдали.// As of Go 1.23, the channel is synchronous (unbuffered, capacity 0),
// eliminating the possibility of those stale values.
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := (*Timer)(newTimer(when(d), 0, sendTime, c, syncTimer(c)))
t.C = c
return t
}
c := make(chan Time, 1)
chan).
Безопасные ворота
Вот три способа безопасно закрыть ворота, один другого краше.
➊ sync.Mutex
Дубовый, но надежный способ. Защищаем изменение логического поля closed мьютексом:
type Gates struct {
// признак закрытия ворот
closed bool
// мьютекс для защиты closed
mu sync.Mutex
}
func (g *Gates) Close() {
g.mu.Lock()
defer g.mu.Unlock()
if g.closed {
// игнорируем повторное закрытие
return
}
// закрыть ворота
g.closed = true
// освободить ресурсы
}type Gates struct {
// признак закрытия ворот
closed atomic.Bool
}
func (g *Gates) Close() {
if !g.closed.CompareAndSwap(false, true) {
// игнорируем повторное закрытие
return
}
// закрыли ворота,
// можно освободить ресурсы
}Once.Do гарантирует однократное выполнение в конкурентной среде, поэтому не приходится даже явно хранить состояние:type Gates struct {
// гарантирует однократное выполнение
once sync.Once
}
func (g *Gates) Close() {
g.once.Do(func() {
// освободить ресурсы
})
}
Помоги Маше закрыть ворота
Друзья, тут такое дело. Маша закрывает ворота вот так:
type Gates struct {
// признак закрытия ворот
closed chan struct{}
}
func (g *Gates) Close() {
select {
case <-g.closed:
// игнорируем повторное закрытие
return
default:
// закрыть ворота
close(g.closed)
}
}Close() из нескольких горутин? Опрос следует.
Обновил курс до Go 1.22
Добавил в базовый курс по Go новые блоки и темы:
— range по целым числам;
— встроенные функции min, max и clear;
— комбинация ошибок через errors.Join;
— причина отмены контекста и context.AfterFunc (+ забористая задачка);
— дженерики, пакеты slices и maps (про них уже писал выше);
— тип-обертка Null в database/sql.
Заодно добавил маленький урок 1.9 со всякой всячиной (пока там блоки, группы и йота).
P.S. А еще на этом уроке вам предстоит победить Сквернолапа. Ну или Когтевика. В крайнем случае — Огнежора 😅
Количество одновременных горутин
Вот конструкция, к которой мы пришли:
func work() {
// работает работу на работе
}
nConc = runtime.NumCPU()*2
sema := make(chan struct{}, nConc)
const n = 100_000
for range n {
sema <- struct{}{}
go func() {
work()
<-sema
}()
}work в основном использует CPU (CPU-bound).work делает HTTP-запросы? Тогда, вероятно, большую часть времени она ожидает ответа от удаленного сервера, и CPU при этом не используется.nConc делают настраиваемым, чтобы можно было менять его в продакшене без перекомпиляции приложения.
Запускаем 100К горутин
Итак, сколько же одновременно живущих горутин можно создать в 1Гб памяти без проблем для работы сервера? Давайте разбираться.
Горутина — это структура минимального размера около 2Кб, так что формально в 1Гб можно уместить 1024*1024/2 ≈ 500K горутин. Понятно, что часть памяти уже занята, но 100 тысяч уж всяко уместятся, так что это ответ 100000+
Но есть нюанс (и даже не один).
Допустим, мы взяли и запустили все горутины, вот так:
func work() {
// пока забудем, что именно
// тут происходит
}
const n = 100_000
for range n {
go work()
}
Приемчики форматирования
Несколько приемов форматирования, о которых вы, возможно, не слышали.
➊ Закавыченная строка
Используйте %q, чтобы вывести строковое значение в кавычках.
s := "Hello, World!"
fmt.Printf("%q\n", s)
// "Hello, World!"
%+v, чтобы вывести названия полей структуры, а не только значения.alice := person{"Alice", 25}
fmt.Printf("%+v\n", alice)
// {name:Alice age:25}%T, чтобы вывести тип значения.var val any
val = 42
fmt.Printf("%T: %v\n", val, val)
// int: 42
val = "Hello, World!"
fmt.Printf("%T: %v\n", val, val)
// string: Hello, World!
val = person{"Alice", 25}
fmt.Printf("%T: %v\n", val, val)
// main.person: {Alice 25}
val выше).num := 42
fmt.Printf("%[1]T: %[1]v\n", num)
// int: 42
range по функциям — что это и зачем
Если вы ничего не поняли из предыдущей заметки — ничего страшного. Давайте обсудим суть новой фичи, не погружаясь в детали реализации.
Предположим, вы написали собственный тип OrderedMap, который (в отличие от обычной карты) сохраняет порядок элементов.
Сделали ему конструктор и метод Set:
m := NewOrderedMap[string, int]()
m.Set("one", 1)
m.Set("two", 2)
m.Set("thr", 3)
m.Range(func(k string, v int) {
fmt.Println(k, v)
})
// one 1
// two 2
// thr 3for k, v := range m.Range {
fmt.Println(k, v)
}
Курсы на Степике
На степике сегодня скидка 20% на все курсы по промо-коду STEPIKSALE20. Так что если давно хотели что-нибудь подучить, сейчас хороший момент, чтобы начать.
И раз такое дело, предлагаю в комментариях делиться ссылками на курсы, которые лично вам очень понравились. И нет, это не завуалированное предложение похвалить мои курсы 😁 Принимается любой личный опыт.
🗒 Узнаем больше о Go разработке 2024
Второй год подряд ребята из DevCrowd проводят большое исследование Go-разработчиков:
- Что входит в обязанности и каких навыков не хватает
- Сколько в среднем зарабатывают в профессии в зависимости от грейда
- Какие инструменты, сервисы наиболее популярны
- Что читают, слушают и смотрят для профессионального развития.
Проходите опрос, делитесь своими мнениями и помогите сделать исследование максимально охватным. Организаторы обещают сравнить данные с прошлым годом и поделиться выводами публично уже в конце ноября.
Результаты опроса помогут вам сравнить свои ожидания с рыночными, построить план своего развития, и просто понять, что происходит с индустрией!
👉 Пройти опрос
👀 Посмотреть результаты прошлого года
Проверки в коде
Есть вот такая функция, которая проверяет разрешения пользователя.
// 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
}
}
Канал завершения
Как вы знаете, с помощью done-канала горутина сигнализирует вызывающему, что закончила работать.
Есть пара вариантов реализации:
➊ Принимаем done-канал на входе
func worker1(done chan struct{}) {
// do work
time.Sleep(10 * time.Millisecond)
close(done)
}done := make(chan struct{})
go worker1(done)
<-donefunc worker2() chan struct{} {
done := make(chan struct{})
go func() {
// do work
time.Sleep(10 * time.Millisecond)
close(done)
}()
return done
}done := worker2()
<-done
Скопировать каталог
Раньше, чтобы рекурсивно скопировать каталог со всем содержимым, вам пришлось бы написать 50 строк кода.
Теперь, благодаря os.CopyFS в Go 1.23, будет достаточно одной:
src := os.DirFS("/home/src")
dst := "/home/dst"
err := os.CopyFS(dst, src)
Пустышки
Вряд ли вы об этом задумывались, но все эти конструкции в Go разрешены:
// ничего не делает
{}
// ничего не делает
switch {}
// бесконечный цикл
for {}
// блокирует горутину
select {}
select{} для быстрых демок — это самый короткий способ заблокировать горутину.func main() {
// тикающая горутина
go func() {
ticker := time.NewTicker(1 * time.Second)
for range ticker.C {
fmt.Print(".")
}
}()
// блокирует горутину main
select {}
}
Читать полностью…
✓ Курс «Многозадачность в Go»
Закончил курс по многозадачности! Вот какие темы в нем разобраны:
— горутины для конкурентного выполнения задач;
— каналы и селект как универсальные инструменты;
— таймеры и тикеры для работы со временем;
— контекст для отмены операций;
— группа ожидания для синхронизации горутин;
— мьютексы для защиты от гонок;
— условные переменные для сигналов о событиях;
— однократное выполнение для безопасной инициализации;
— пулы, чтобы снизить нагрузку на сборщик мусора;
— атомарные операции.
Если вы совсем не знакомы с многозадачностью, курс поможет освоить ее с нуля. А если уже прошли модуль «Многозадачность» на курсе «Go на практике» — детально разберетесь в гонках, синхронизации и пакетах sync и atomic.
Как обычно, все концепции разобраны на практических примерах и закреплены задачками с автоматической проверкой.
Курс непростой. Подойдет практикующим разработчикам с уверенным знанием основ Go.
https://stepik.org/a/133280
Цена скоро вырастет.
Всем go 💪
Git в примерах
Зашел я на Степик в новые курсы, а там «Основы Git». В связи этим вспомнил, что у меня тоже есть интерактивная книга / сборник рецептов по гиту, называется Git by example.
Удобный краткий формат с конкретными примерами, я сам туда постоянно подглядываю (особенно в раздел undo).
Загляните и вы, если не обзавелись еще черным поясом по гиту.
https://antonz.org/git-by-example
P.S. А вы же знаете, что по Go тоже такая есть?
Опасные ворота
Вот наши ворота:
type Gates struct {
// признак закрытия ворот
closed chan struct{}
}
func (g *Gates) Close() {
select {
case <-g.closed:
// игнорируем повторное закрытие
return
default:
// закрыть ворота
close(g.closed)
// освободить ресурсы
}
}Close — небезопасный. Если две горутины одновременно вызовут Close, обе могут провалиться в default-ветку селекта, обе попытаются закрыть канал, и второе закрытие приведет к панике.
Баян [::]
Гошный баян (вообще он «полносрезное выражение» или full slice expression, но «баян» мне ближе) имеет такой синтаксис:
s[low : high : max]
high-low и емкостью max-low. Используется крайне редко.arr := [5]int{1, 2, 3}
// [1 2 3 0 0]
s := arr[0:3]
// [1 2 3]
len(s) // 3
cap(s) // 5s указывает на массив arr. Его длина (length) равна 3, а емкость (capacity, размер массива под срезом) равна 5.s = append(s, 4)
fmt.Println(arr)
// [1 2 3 4 0]
fmt.Println(s)
// [1 2 3 4]
arr := [5]int{1, 2, 3}
// [1 2 3 0 0]
s := arr[0:3:3]
// [1 2 3]
len(s) // 3
cap(s) // 3arr не изменится:s = append(s, 4)
fmt.Println(arr)
// [1 2 3 0 0]
fmt.Println(s)
// [1 2 3 4]
Пустой срез vs. nil-срез
Как вы знаете, объявленная без инициализации переменная в Go автоматически получает нулевое значение соответствующего типа:
var num int // 0
var str string // ""
var flag bool // false
var snil []int
// []int(nil)
sempty := []int{}
// or
// sempty = make([]int, 0)reflect.DeepEqual(snil, sempty)
// false
len(snil) // 0
cap(snil) // 0
snil = append(snil, 1) // []int{1}
len(sempty) // 0
cap(sempty) // 0
sempty = append(sempty, 1) // []int{1}
reflect.DeepEqual(snil, sempty)
// true
Создаем 100К горутин, выполняем N
Итак, мы создаем 100 тысяч горутин.
Разумно при этом ограничить количество одновременно выполняющихся горутин, чтобы не пытаться впихнуть их на CPU все разом:
nConc = runtime.NumCPU()*2
sema := make(chan struct{}, nConc)
const n = 100_000
for range n {
go func() {
sema <- struct{}{}
work()
<-sema
}()
}
nConc.sema <- struct{}{} из горутины в общий цикл, то и создаваться горутины будут не все разом, а постепенно.
Горутинки
Допустим, у вас есть сервер с 1Гб памяти. Вы выполняете на нем какие-то задачки, и для каждой задачки создаете горутину. Внутри горутина очень простая, без рекурсий и жирных объектов в памяти:
func work() int {
res := httpGet(url)
return res
}
Куда идет Go
Стоило ли добавлять в язык range-over-func? На мой взгляд — нет.
Сила Go — в дубовости. У нас уже есть много фичастых языков с большой внутренней сложностью (Java, C#, C++), и Go был единственной простой мейнстрим-альтернативой для них.
Да, Go не модный, и код на нем простынявый. Но он простой до примитивности, и это хорошо.
С появлением дженериков в 1.18 по простоте языка был нанесен серьезный удар (вероятно, оправданный). Теперь же авторы продолжают усложнять язык без веских на то причин, просто чтобы было красивенько.
Почему так происходит?
Есть подозрение, что в команде разработки Go заработал стандартный для гугла процесс — promotion-driven development. Развитием языка управляют люди, которым надо демонстрировать выполнение KPI — а значит, будут поливать Go фичами из шланга, превращая в еще одну джаву.
Ждут нас и корутины, и стримы, и паттерн-матчинг.
Штош.
range по функциям
В Go 1.23 (запланирован в августе) появится цикл range по функциям. Проще всего показать на примере.
Допустим, мы написали функцию, которая принимает срез, и возвращает его инвертированную версию, не меняя при этом оригинал:
func Backward[E any](s []E) []E {
r := make([]E, 0, len(s))
for i := len(s) - 1; i >= 0; i-- {
r = append(r, s[i])
}
return r
}s := []string{"a", "b", "c"}
for _, el := range Backward(s) {
fmt.Print(el, " ")
}
// c b aBackward создает копию исходного среза. Для abc это не проблема, но в целом не слишком-то эффективно с точки зрения использования памяти.func Backward[E any](s []E) func(func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}s := []string{"a", "b", "c"}
for _, el := range Backward(s) {
fmt.Print(el, " ")
}
// c b a