Pull to refresh

Перевод: Один год с Go

Reading time6 min
Views51K
Original author: Andrew Thompson
Под катом — перевод статьи опытного разработчика о его опыте практического применения Go. Важно — мнение переводчика может не совпадать с мнением автора статьи.




Итак, прошел год с тех пор, как я начал использовать 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.
Tags:
Hubs:
+90
Comments304

Articles