![](https://habrastorage.org/files/a18/7de/a26/a187dea263874a3c8f07166c844c209e.png)
Итак, прошел год с тех пор, как я начал использовать Go. Неделю назад я удалил его из production.
Я пишу этот пост, потому что в течение последнего года многие спрашивали меня о впечатлениях от работы с Go, и мне бы хотелось рассказать о нем несколько больше, чем это возможно в Twitter и IRC — пока воспоминания не выветрились у меня из памяти.
Итак, поговорим о том, почему я не считаю Go полезным инструментом:
Инструментарий
Инструменты, поставляемые с Go, странные. На первый взгляд многие из них неплохи, но при длительной работе большинство из них начинают демонстрировать свои ограничения. По сравнению с инструментарием для C или Erlang они похожи на не очень удачную шутку.
анализ покрытия
Строго говоря, утилита для анализа покрытия кода в Go — это хак. Она работает только с одним файлом за раз и делает это, вставляя в него примерно вот такой код:
GoCover.Count[n] = 1
Где n — это идентификатор позиции ветвления в файле. Ну а еще она вставляет в конец файла вот такую гигантскую структуру:
действительно гигантская
var GoCover = struct {
Count [7]uint32
Pos [3 * 7]uint32
NumStmt [7]uint16
} {
Pos: [3 * 7]uint32{
3, 4, 0xc0019, // [0]
16, 16, 0x160005, // [1]
5, 6, 0x1a0005, // [2]
7, 8, 0x160005, // [3]
9, 10, 0x170005, // [4]
11, 12, 0x150005, // [5]
13, 14, 0x160005, // [6]
},
NumStmt: [7]uint16{
1, // 0
1, // 1
1, // 2
1, // 3
1, // 4
1, // 5
1, // 6
},
}
Такой подход работает адекватно для простых юнит тестов в рамках одного файла, но если вы захотите получить анализ покрытия для большого интеграционного теста — могу только пожелать вам удачи. идентификаторы в глобальной области видимости конфликтуют между файлами, а при использовании уникальных имен нет простого способа получить общий отчет о покрытии. Для других языков программирования решения для анализа покрытия кода работают с программой в целом, а никак не для отдельных файлов.
анализ производительности
То же самое с утилитой для анализа производительности — выглядит хорошо, пока не посмотришь как она работает. А работает она путем оборачивания кода в цикл с переменным количеством итераций. После чего цикл итерируется пока код не выполняется «достаточно долго» (по умолчанию 1 секунда), после чего общее время выполнения делится на количество итераций. Такой подход не только включает в замер производительности сам цикл, но также скрывает флюктуации. Код реализации из benchmark.go:
func (b *B) nsPerOp() int64 {
if b.N <= 0 {
return 0
}
return b.duration.Nanoseconds() / int64(b.N)
}
Эта реализация замаскирует паузы сборщика мусора, замедление связанное с гонками выделения ресурсов и другие интересные вещи, если они случаются не слишком часто.
компилятор и go vet
Одна из обсуждаемых сильных сторон Go — это быстрая компиляция. Насколько я могу сказать, это частично достигается за счет того, что многие проверки, которые обычно делает компилятор просто пропускаются — они реализованы в go vet. Компилятор не проверяет проблемы с одинаковыми именами переменных в разных областях видимости или с некорректным форматом printf, все эти проверки реализованы в go vet. Более того, качество проверок ухудшается в новых версиях: в версии 1.3 не показывает проблемы, которые показывала версия 1.2.
go get
Пользователи go хором говоря не пользоваться get, но при этом ничего не делают, чтобы пометить ее как неудачную реализацию и сделать официальную замену.
$GOPATH
Еще одна идея, от которой я не в восторге. С гораздо большим удовольствием я бы клонировал проект в домашнюю директорию и использовал систему сборки, чтобы установить все нужные зависимости. Не то чтобы реализация $GOPATH доставляла много хлопот, но это неприятная мелочь, о которой приходится помнить.
Go race detector
Вот это — неплохая штука. Огорчает что оно вообще нужно. Ну и тот факт, что работает не на всех поддерживаемых платформах (FreeBSD, кто-нибудь?) и максимальное количество goroutine всего 8192. Более того, необходимо умудриться столкнуться с race condition — что довольно трудно, учитывая насколько race detector все замедляет.
Рантайм
Channels/mutexes
Каналы и мьютексы МЕДЛЕННЫЕ. Добавление синхронизации через мьютексы на production настолько снизило скорость работы, что лучшим решением стал запуск процесса под daemontools и его перезапуск в случае падения.
логи падений
Когда go падает, все без исключения goroutine отгружают свой стек вызова в stdout. Объем этой информации растет с ростом вашей программы. Более того, многие сообщения об ошибках косноязычно сформулированы, к примеру ‘evacuation not done in time’ или ‘freelist empty’. Создается впечатление что авторы этих сообщений задались целью максимизировать трафик к поисковому движку google, потому что в большинстве случаев это единственный способ понять что происходит.
интроспекция runtime
Не работает, на практике Go поддерживает в живых концепцию «отладка принтами». Можно использовать gdb, но не думаю что вы захотите это делать.
Язык
Я не получаю удовольствия от написания кода на Go. Я либо сражаюсь с ограниченной системой типов с кастами всего в interface{} либо занимаюсь копипастой кода который делает практически одно и то же для разных типов. Каждый раз, когда я добавляю новую функциональность это выливается в определение еще большего количества типов и допиливания кода для работы с ними. Чем это лучше использования C с адекватными указателями, или использования функционального кода со сложными типами?
Судя по всему, у меня также проблемы с пониманием указателей в Go (с C таких проблем нет). Во множестве случаев добавление звездочки в код волшебным образом заставляло его работать, несмотря на то, что компилятор без ошибок компилировал оба варианта. Почему я должен работать с указателями, используя язык со сборщиком мусора?
Проблемы вызывает преобразование byte[] в string и работа с массивами/слайсами. Да, я понимаю для чего все это было сделано, но по моим ощущениям оно слишком низкоуровневое по отношению к остальному языку.
А еще есть [:],... с append. Посмотрите на это:
iv = append(iv, truncatedIv[:]...)
Этот код требует ручного контроля, потому что append в зависимости от размера массива либо добавит значения, либо сделает перевыделение памяти и вернет новый указатель. Здравствуй, старый добрый realloc.
стандартная библиотека
Часть стандартной библиотеки неплоха, особенно криптография, которая в лучшую сторону отличается от простой обертки над OpenSSL, которую вам предлагает большинство языков. Но документация и все что связано с интерфейсами… Мне часто приходится читать код реализации вместо документации, потому что последняя часто ограничивается бесполезным «имплементирует метод X».
Большие проблемы вызывает библиотека 'net'. В отличие от обычных сетевых библиотек, эта библиотека не позволяет менять параметры создаваемых сокетов. Хотите установить флаг IP_RECVPKTINFO? Используйте библиотеку 'syscall', которая является самой плохой оберткой над POSIX из всех, которые я видел. Нельзя даже получить файловый дескриптор созданного соединения, все приходится делать через 'syscall':
Скрытый текст
fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_DGRAM, 0)
if err != nil {
rlog.Fatal("failed to create socket", err.Error())
}
rlog.Debug("socket fd is %d\n", fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IPV6, syscall.IPV6_RECVPKTINFO, 1)
if err != nil {
rlog.Fatal("unable to set IPV6_RECVPKTINFO", err.Error())
}
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, 1)
if err != nil {
rlog.Fatal("unable to set IPV6_V6ONLY", err.Error())
}
addr := new(syscall.SockaddrInet6)
addr.Port = UDPPort
rlog.Notice("UDP listen port is %d", addr.Port)
err = syscall.Bind(fd, addr)
if err != nil {
rlog.Fatal("bind error ", err.Error())
}
А еще вы получите море удовольствия, получая и передавая byte[] при вызове 'syscall' функций. Создание и удаление C структур из Go — это просто какой-то заряд позитива.
Возможно, так сделано для использования сокетов только в polling сценариях? Я не знаю, но любая попытка сложного сетевого программирования приводит к необходимости писать ужасный и непортабельный код.
Выводы
Я не могу понять смысл Go. Если мне нужен системный язык, я использую C/D/Rust. Если мне нужен язык с хорошей поддержкой параллелизма, то я использую Erlang или Haskell. Единственное применение Go, которое я вижу — это утилиты командной строки, которые должны быть портабельны и не тянуть за собой зависимости. Я не думаю, что язык хорошо подходит для «долгоживущих» серверных задач. Возможно, он выглядит привлекательно для Ruby/Python/Java разработчиков, откуда, насколько я понимаю, и пришло большинство разработчиков на Go. Я также не исключаю, что Go станет «новой Java», учитывая легкость развертывания и репутацию языка. Если вы ищите более хорошую версию Ruby/Python/Java, возможно, Go вам подойдет — но я бы не рекомендовал останавливать свой поиск на этом языке. Хорошие языки программирования позволяют вам развиваться как программисту. LISP демонстрирует идею «код как данные», «C» обучает работе с компьютером на низком уровне, Ruby показывает работу с сообщениями и анонимными функциями, Erlang рассказывает про параллелизм и отказоустойчивость, Haskell демонстрирует настоящую систему типов и работу без побочных эффектов, Rust позволяет понять как разделять память для параллельно выполняемого кода. Но я не могу сказать, что научился чему-то, используя Go.