Мне нравится изучать что-то новое, особенно новые языки. Всегда интересно узнать, как с одной и той же проблемой справляются разные языки и как различается их синтаксис. Ознакомившись с многочисленными шаблонами и решениями, без труда распознаёшь их в новом для себя языке программирования, и это облегчает его освоение.
Как-то раз решил я заняться языком Go. Здесь меня, привыкшего к потокам в Java, сопоставлению с образцом текста в Scala и функциональной библиотеке Kotlin, поджидало несколько сюрпризов. Это было как погружение в параллельную реальность, где нет лямбд, а циклы for царствуют безраздельно. Я расскажу, к чему мне пришлось привыкать, чего мне недоставало в Go и как Go этот недостаток восполняет, а также что здесь есть такого, чего мне не хватало в Java.
Настройка среды разработки в Go отличается от того, к чему мы привыкли в виртуальной машине Java. Все программы должны быть в одном месте, в структуре такого вида:
Go ← вот на что должна указывать переменная среды GOPATH
| — bin
| — pkg
| — src
| - github.com
| - gosoup
| - goworkshop
| - myprivatestuff
| - evilplans
Вы помещаете весь код src (обычно в папку github.com или myprivatestuff).
Вам также нужна вторая переменная среды под названием GOROOT. Она должна указывать туда, где вы установили Go. Но не верьте мне на слово, лучше сами проверьте, следуя указаниям официальной документации: https://golang.org/doc/install
ЭкспортированиеЭкспортирование в Go происходит очень просто: переменные и функции должны начинаться с заглавной буквы, тогда они доступны другим пакетам. Вот и всё. Из этих двух функций, находящихся в пакете gosoup
.
package gosoup
func mySecretLittleFunction() {}
func UseMeImExported() {}
…первая у нас внутренняя, а вторая — экспортируемая. Когда я пишу:
strings.ToLower("HELLO")…это означает, что я вызываю экспортируемую функцию ToLower
в пакете strings
.
Go — язык очень строгий: здесь нет рекомендаций, есть только правила. И для соблюдения этих правил даются два инструмента: gofmt и golint. Вы можете запустить их в терминале или интегрировать в IDE, и тогда они будут ругаться всякий раз, когда вы будете нарушать правила и писать неправильный код.
Вот строчка кода с неправильным форматированием:
package gosoup func Not_well_formatted() {println("hello")}Запустив gofmtв этом файле, мы получим правильное форматирование кода:
$ gofmt gofmttest.go
package gosoup
func Not_well_formatted() { println(“hello”) }
Запустив golint в этом файле, мы получим разъяснения:
$ golint gofmttest.go
gofmttest.go:3:1: exported function Not_well_formatted should have comment or be unexported
gofmttest.go:3:10: don’t use underscores in Go names; func Not_well_formatted should be NotWellFormatted
1) у экспортируемой функции не хватает комментария, без которого она становится неэкспортируемой; 2) нельзя использовать нижнее подчёркивание в названиях Go.
ТестированиеВ Go предусмотрен тестовый пакет, который можно использовать для написания юнит-тестов. Есть также библиотека для проверки утверждений, но для простоты вы можете просто сравнивать свои тестовые ожидания, используя if.
При написании тестов надо следовать следующим правилам:
t *testing.T
.Вот вам пример теста, в котором вызывается функция с ожидаемым результатом 8:
func TestGetANumber_(t *testing.T) {
want := 8
got := GetANumber()
if got != want {
t.Fatalf(`Fail! Wanted '%v', got '%v'`, want, got)
}
}
Теперь можете запустить этот тест в терминале:
go test ./gosoup_test.go
— — FAIL: TestGetANumber_ (0.00s)
gosoup_test.go:13: Fail! Wanted ‘8’, got ‘7’
В Go нельзя наследовать классы, просто потому что их там нет. Вероятно, ближе всего к классам структуры и интерфейсы, но и их в Go наследовать все равно нельзя. Есть отличная статья о наследовании в Go, в которой предлагается, помимо других решений, использовать композицию (технику, которую иногда применяют и в Java):
type Cat struct {
name string
attributes string
}
type PersianCat struct {
Cat
specialAttributes string
}
func main() {
plato := PersianCat{
Cat: Cat{
name: "Plato",
attributes: "four-legs, whiskery, destroyer of sofas",
},
specialAttributes: "long-haired, flat-nosed",
}
println(plato)
}
Исключения
У меня неоднозначное отношение к исключениям в Java. Да, они полезны, но их бывает сложно обработать. В последнее время все известные мне библиотеки перешли на исключения во время выполнения, благодаря чему код становится более читаемым, а исключения ещё неуловимее. В Go не используются исключения, здесь функции могут возвращать несколько значений: одно в качестве результата выполнения функции, другое в качестве флага ошибки. При вызове такой функции вы присваиваете две переменные результату функции и проверяете переменную типа error
на предмет того, всё ли прошло хорошо. Однако, как и в случае с исключениями во время выполнения, здесь от разработчика требуется максимум дисциплины, чтобы проверять эти ошибки и соответствующим образом на них реагировать:
func Divide(x float32, y float32) (float32, error) {
if y == 0 {
return 0, errors.New("division by zero") // произошла ошибка
}
return x / y, nil // результат без ошибки
}
func DivisionRunner(x float32, y float32) {
result, err := Divide(x, y)
if err == nil {
fmt.Printf("%v / %v = %v", x, y, result)
} else {
fmt.Printf("error: %v", err)
}
}
Дженерики
Дженерики — это отличный способ не писать кучу вариаций одной и той же функции, которые отличаются друг от друга лишь используемыми типами. Такие коллекции, как массивы, срезы и карты поддерживают дженерики в Go, но написать собственные универсальные или настраиваемые функции здесь будет уже проблематично. Кто-то предлагает создавать подобные функции с использованием интерфейсного типа, но также есть довольно много предложений добавить в Go поддержку дженериков, так что посмотрим, появятся ли в будущем в Go дженерики, либо мы будем довольствоваться обходными решениями.
ЛямбдаВсе мы любим лямбда-выражения и особенно операции с потоками map, filter и collect. Но как их использовать в Go? Краткий ответ: никак. Go не такой уж функциональный язык — немного практики и к этому привыкаешь. Для Go характерно стремление к упрощению с использованием циклов for и конструкций if/else. Вот пример, написанный в типичном для Go стиле: начать с пустой переменной, выполнить итеративный обход коллекции, проверить условие, добавить элементы к предыдущему списку и, наконец, вернуть этот список.
func filterRhymingWords(words []string, suffix string) []string {
var rhymingWords []string
for _, word := range words {
if strings.HasSuffix(word, suffix) {
rhymingWords = append(rhymingWords, word)
}
}
return rhymingWords
}
Такой стандартизированный код лёгок для чтения и сопровождения и узнаваем для других разработчиков. Программисты, работающие на Go, обычно гордятся тем, что у них предсказуемый код и простой синтаксис.
Тесты производительности — это замечательная встроенная функциональная возможность Go, позволяющая запускать юнит-тесты производительности над одной функцией. Предположим, ваша функция выглядит следующим образом:
func GetANumber() int {
rand.Seed(time.Now().UnixNano()) // каждый раз разное начальное значение
return rand.Intn(100) // генерируем случайное число
}
Вы можете написать для неё вот такой тест производительности:
func BenchmarkGetANumber(b *testing.B) {
for i := 0; i < b.N; i++ {
GetANumber()
}
}
Функция будет запускаться многократно, пока время выполнения не стабилизируется. Результат теста будет показан примерно в таком виде:
BenchmarkGetANumber_-12 169064 7059 ns/opЭто означает, что функция запускалась 169064 раза на 12 программах, причём каждый запуск проходил в среднем 7059 наносекунд (довольно много с учётом случайного характера начальных значений). Создавать такие тесты производительности для функций можно (и нужно), чтобы выявлять те из них, которые работают подозрительно медленно.
Несколько возвращаемых значенийМетоды в Java возвращают всегда одно единственное значение. Хотите больше — придётся создать объект и инкапсулировать в него эти значения. В других языках виртуальной машины Java, таких как Scala и Kotlin, есть кортежи, позволяющие создавать специально подобранные объекты с этими значениями внутри. Эта функциональная особенность Go очень полезна и используется, например, для возвращения значения и ошибки. Ну, а вы можете использовать её и для других целей:
func GetProtocolDomainAndPort(url string) (string, string, int) {
// ...находим эти части
return protocol, domain, port
}
Добавление функций к имеющимся структурам
И снова в Java этого сделать нельзя, а Kotlin (и другие языки, подобные Javascript) позволяют добавлять функциональность к уже определённым структурам. В приведённом ниже примере мы определяем структуру, а затем добавляем к ней новую функцию. Далее создаём экземпляр структуры и, наконец, вызываем в нём эту новую функцию:
type Person struct {
firstName string
surname string
}
func (person Person) fullName() string {
return fmt.Sprintf("%v %v", person.firstName, person.surname)
}
func main() {
arnold := Person{"Arnold", "Schwarzenegger"}
println(arnold.fullName())
}
Go славится простотой и ясностью кода, которые помогают сделать его разработку и сопровождение простыми и предсказуемыми. В отличие от других языков, в Go вы можете вернуться к своему коду (или коду, над которым работал другой разработчик) через несколько дней, недель или даже месяцев и без проблем продолжить работу над ним. Вам не придётся тратить драгоценное рабочее время на расшифровку сложных однострочных скриптов: всё будет ясно и понятно. С помощью Go вы также сможете создавать лёгкие, надёжные и высокопроизводительные программы. Если всё это важно для вас и вашей команды, возможно, Go — это то, что вам нужно.
Перевод статьи Uzi Landsmann: A Java developer’s adventures through the strange landscape of Go
Комментарии