- PVSM.RU - https://www.pvsm.ru -
Ваш любимый питомец пишет на Go и получает больше вас, а вы ещё нет? Не теряйте времени… Такая мысль может родиться у читателя от обилия статей по Go. Некоторым даже компании предлагают переучиться на этот язык. И, если вы хоть раз задумывались освоить язык, то я хочу вас предостеречь. Вернее показать странные вещи, попробовать объяснить зачем они и потом вы уже сами сделаете вывод нужен ли вам Go.
Я сам Си++/Python разработчик и могу сказать, что это сочетание является один из оптимальнейших для освоения Go. И вот почему:
Что по-поводу пары Java/C#? Go ей ни разу не конкурент, по крайней мере пока он молод (речь про версию Go 1.11).
А что будет? Только конкретные случаи дискомфорта, которые доставляет язык в работе.
Хорошим вводным по языку мануалом является короткая онлайн книга Введение в программирование на Go [1]. Читая которую вы довольно быстро наткнётесь на странные особенности. Приведём для начала первую партию из них:
package main
func main() // Не компилируется
{
}
Авторы считают, что стиль программирования должен быть единообразным и компактным. Чтож хозяин — барин.
a := []string{
"q" // Нет запятой, не компилируется
}
Видимо здесь боятся пулл-реквестов, где будет изменение в двух строках при добавлении одной строки в конец.
package main
func main() {
a := []string{
"q",
}
// Не компилируется, переменная не использована
}
Здесь упор идёт на то, что почти всегда это ошибка, связанная или с опечаткой, или спешкой, или кривым рефакторингом. Как бы в конечном коде да, такого быть не должно. Но мы редко пишем сразу конечный код и периодически пробуем запускать промежуточные версии, в которых может быть некоторый задел на будущее. Поэтому данное поведение компилятора напрягает.
Правда со временем возникает множество ситуаций, когда это уберегло от ошибки. Но это всё-равно напрягает.
Неиспользуемые параметры приходится заглушать и это смотрится странно, хотя в питоне так тоже можно:
for _, value := range x {
total += value
}
Но это всё цветочки и даже просто вкусовщина разработчиков. Теперь перейдём к более тяжеловесным вещам.
И тут надо не забыть сказать об очень важной вещи. Дело в том, что язык сделан именно таким, чтобы неопытным разработчики не имели возможности создавать плохие программы.
Вот цитата одного из создателей языка:
«Ключевой момент здесь, что наши программисты (прим.пер.: гуглеры) не исследователи. Они, как правило, весьма молоды, идут к нам после учебы, возможно изучали Java, или C/C++, или Python. Они не в состоянии понять выдающийся язык, но в то же время мы хотим, чтобы они создавали хорошее ПО. Именно поэтому язык должен быть прост для понимания и изучения.»
Спионерено отсюда: Почему дизайн Go плох для умных программистов [3].
Так значит вы говорите безопасный язык?
var x map[string]int
x["key"] = 10
и после запуска программы получаем:
panic: runtime error: assignment to entry in nil map
В этом невинном примере мы «забыли» выделить себе память и получили ошибку времени выполнения. Так а какой безопасности может идти речь, если вы меня не спасли от неверной ручной работы по выделению ресурсов?
читатель tyderh [4] замечает, что:
Безопасность заключается в том, что при выполнении отлавливается ошибка, а не происходит неопределённое поведение, способное произвольным образом изменить ход выполнения программы. Таким образом, подобные ошибки программистов не способны привести к появлению уязвимостей.
Следующий пример:
var i32 int32 = 0
var i64 int64 = 0
if i64 == i32 {
}
Вызовет ошибку компиляции, что как бы нормально. Но поскольку в Go пока (пока!) нет шаблонов, то очень часто они эмулируются через интерфейсы, что может рано или поздно вылиться в такой код:
package main
import (
"fmt"
)
func eq(val1 interface{}, val2 interface{}) bool {
return val1 == val2
}
func main() {
var i32 int32 = 0
var i64 int64 = 0
var in int = 0
fmt.Println(eq(i32, i64))
fmt.Println(eq(i32, in))
fmt.Println(eq(in, i64))
}
Этот код уже компилируется и работает, но не так как ожидает программист. Все три сравнения выдадут false, ибо сначала сравнивается тип интерфейсов, а он разный. И если в данном случае ошибка явно бросается в глаза, в реальности она может быть сильно размыта.
Ну и завершая про безопасность. Разыменование в языке убрано, а вот спецэффекты в зависимости от вида доступа от доступа (по указателю или по копии) остались. Поэтому следующий код:
package main
import "fmt"
type storage struct {
name string
}
var m map[string]storage
func main() {
m = make(map[string]storage)
m["pen"] = storage{name: "pen"}
if data, ok := m["pen"]; ok {
data.name = "-deleted-"
}
fmt.Println(m["pen"].name) // Output: pen
}
Выведет pen. А следующий:
package main
import "fmt"
type storage struct {
name string
}
var m map[string]*storage
func main() {
m = make(map[string]*storage)
m["pen"] = &storage{name: "pen"}
if data, ok := m["pen"]; ok {
data.name = "-deleted-"
}
fmt.Println(m["pen"].name) // Output: -deleted-
}
Выведет "-deleted-", но пожалуйста, не ругайте сильно программистов, когда они на эти грабли наступят, от этого в «безопасном» языке их не спасли.
m = make(map[string]storage)
а в другом:
m = make(map[string]*storage)
Ха, вы думали всё? Я тоже так думал, но неожиданно напоролся ещё на одни грабли:
package main
import "fmt"
var globState string = "initial"
func getState() (string, bool) {
return "working", true
}
func ini() {
globState, ok := getState()
if !ok {
fmt.Println(globState)
}
}
func main() {
ini()
fmt.Println("Current state: ", globState)
}
Возвращает initial и это верно ибо оператор := создаёт новые локальные переменные. А его мы вынуждены были использовать из-за переменной ok. Опять таки всё верно, но изначально строчка
globState, ok := getState()
могла выглядеть как
globState = getState()
а потом вы решили добавить второй параметр возврата, IDE подсказал вам, что теперь надо его ловить, и вам пришлось попутно заменить оператор и вдруг вы видите грабли перед лицом.
А это значит, что теперь нам надо у PVS просить статический анализатор для языка Go.
Краткий вывод: безопасность присутствует, но она не абсолютна от всего.
Выше в разделе странности компилятора было указано, что при неверном форматировании кода, компилятор упадёт. Я предположил, что это было сделано для единообразия кода. Посмотрим насколько код единообразный.
Вот например, два способа выделить память:
make([]int, 50, 100)
new([100]int)[0:50]
Ну да, ну да, это просто фишка функции new, которую мало кто использует. Ладно будем считать это не критичным.
Вот например, два способа создать переменную:
var i int = 3
j := 6
Ладно, ладно, var используется реже и в основном для резервирования под именем определённого типа или для глобальных переменных неймспейса.
Ладно, с натяжкой будем считать Go единообразным языком.
А вот ещё частая проблема, конструкция вида:
result, err := function()
if err != nil {
// ...
}
Это типичный кусок кода на Go, назовём его условно колбасой. Среднестатистический код на Go состоит на половину из таких колбас. При этом первая колбаса сделана так result, err := function(), а все последующие так result, err = function(). И в этом не было бы проблемы, если бы код писался только один раз. Но код — штука живая и постоянно приходиться менять местами колбасы или утаскивать часть колбас в другое место и это вынуждает постоянно менять оператор := на = и наоборот, что напрягает.
Когда читаешь книгу по Go, не перестаёшь удивляться компактности, кажется что все конструкции продуманы так, чтобы код занимал как можно меньше места как по высоте, так и по ширине. Эта иллюзия быстро рушится на второй день программирования.
И в первую очередь из-за «колбас», о которых я упоминал чуть выше. Сейчас ноябрь 2018 и все Go программисты ожидают версию 2.0, потому что в нём будет новая обработка ошибок [5], которая наконец покончит с колбасами в таком количестве. Рекомендую статью по ссылке выше, в ней суть проблемы «колбасного» кода разъяснена наглядно.
Но новая обработка ошибок не устранит все проблемы компактности. По прежнему будет не хватать конструкций in и not in. На текущий момент проверка нахождения в map значения выглядит так:
if _, ok := elements["Un"]; ok {
}
И единственное на что можно надеяться — на то, что после компиляции это будет скукожено до просто проверки значения, без инициализации попутных переменных.
К Go существует очень много написанного кода. И есть просто потрясающие вещи. Но не редко вы выбираете между очень плохой библиотекой и просто приемлемой. Например SQL JOIN в одном из лучших ORM в GO (gorm) выглядит так:
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)
А в другом ORM вот так:
query := models.DB.LeftJoin("roles", "roles.id=user_roles.role_id").
LeftJoin("users u", "u.id=user_roles.user_id").
Where(`roles.name like ?`, name).Paginate(page, perpage)
Что ставит пока под сомнение вообще необходимость использовать ORM ибо нормальной поддержки защиты от переименования полей не везде просто нет. И ввиду компилируемой природы языка может и не появиться.
А вот один из лучших образцов компактного роутинга в вебе:
a.GET("/users/{name}", func (c buffalo.Context) error {
return c.Render(200, r.String(c.Param("name")))
})
Не то чтобы здесь было что-то плохое, но в динамических языках код обычно выглядит более выразительным.
Угадайте, как сделать функцию публичной для использования в других пакетах? Здесь есть два варианта: либо вы знали или никогда бы не угадали. Ответ: зарезервированного слова нет, нужно просто назвать функцию с большой буквы. В это вляпываешься ровно один раз и потом привыкаешь. Но как питонист помню про правило «явное лучше неявного» и предпочёл бы отдельное зарезервированное слово (хотя если вспомнить про двойное подчёркивание в питоне, то чья бы корова мычала).
Если вам нужен словарь объектов, то вы напишите что-то такое:
elements := map[string]map[string]string{
"H": map[string]string{
"name": "Hydrogen",
"state": "gas",
},
}
Пугающая конструкция, не правда ли? Глазу хочется каких-нибудь скобочек, чтобы не спотыкаться. К счастью они возможны:
elements := map[string](map[string]string){
}
Но это всё, что позволит вам форматтер go fmt, который почти наверняка будет использоваться в вашем проекте для переформатирования кода при сохранении. Все остальные вспомогательные пробелы будут выпилены.
Их нет. Для синхронизации надо явно использовать мьютексы и каналы. Но «безопасный язык» не будем вам пытаться мешать писать одновременно из разных потоков в стандартные структуры и получать падение программы.
Во всех не очень безопасных языках безопасность хорошо реализуется через тестирование с хорошим покрытием. В Go с этим почти всё в порядке, кроме необходимости писать колбасы в тестах:
if result != 1 {
t.Fatalf("result is not %v", 1)
}
Понимая ущербность данного подхода, мы сразу нашли в сети библиотеку, реализующую assert и доработали её до вменяемого состояния. Можно брать и использовать: https://github.com/vizor-games/golang-unittest [6].
Теперь тесты выглядят так:
assert.NotEqual(t, result, 1, "invalid result")
В языке сущность интерфейса используется, чтобы заткнуть «бедность» синтаксиса языка. Выше уже был пример с реализацией шаблонов через интерфейсы и неявным вредным спецэффектом, порождённым этим случаем. Вот ещё один пример из этой же серии.
Для преобразования типов можно использовать обычную конструкцию в Си-стиле:
string([]byte{'a'})
Но не пытайтесь применить её к интерфейсам, ибо для них синтаксис другой:
y.(io.Reader)
И это довольно долго будет вас путать. Я для себя нашёл следующее правило для запоминания.
Преобразование слева называется conversion [7], его корректность проверяется при компиляции и в теории для констант может производится самим компилятором. Такое преобразование аналогично static_cast из Си++.
Преобразование справа называется type assertion [8] и выполняется при выполнении программы. Аналог dynamic_cast в Си++.
vgo одобрен [9], поддерживается JetBrains GoLand 2018.2 [10], для остальных IDE как временное решение подойдёт команда:
vgo mod -vendor
Да, это выглядит как небольшой костыль сбоку, но это отлично работает и просто реализует ваши ожидания по версионированию. Возможно в go2 этот подход будет единственным и нативным.
В версии 1.11 [11] эта штука уже встроена в сам язык. Так что верной дорогой идут товарищи.
Прочитав статью может возникнуть предположении, что над нами стоит надсмотрщик с плёткой и заставляет писать на Go, исключительно ради наших страданий. Но это не так, в языке есть фишки существенно перевешивающие все вышеописанные недостатки.
package main
import (
"fmt"
"os"
"log"
)
func main() {
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
b, err := ioutil.ReadAll(file)
fmt.Print(b)
}
Благодаря
defer file.Close()
мы сразу сообщаем рантайму, что, в независимости от того каким и где будет выход из функции, в конце надо выполнить определённый код. Это сразу решает проблему с отсутствием деструкторов и контекстов (например питоновский with).
Go выглядит как надмножество Си. Об этом говорит очень многое: и похожесть синтаксиса и понимание того, как это может быть легко преобразовано в Си код. Конечно же горутины, сборка мусора и интерфейсы (а вместе с ним RTTI) нетипичны для Си, но весь остальной код легко конвертируется практически регулярками.
И вот эта природа, на мой взгляд, и диктует почти все приведённые выше странности.
Автор: gnomeby
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/302841
Ссылки в тексте:
[1] Введение в программирование на Go: http://golang-book.ru/
[2] египетские скобки: https://habr.com/post/96978/
[3] Почему дизайн Go плох для умных программистов: https://habr.com/post/344356/
[4] tyderh: https://habr.com/users/tyderh/
[5] новая обработка ошибок: https://habr.com/post/422049/
[6] https://github.com/vizor-games/golang-unittest: https://github.com/vizor-games/golang-unittest
[7] conversion: https://golang.org/ref/spec#Conversions
[8] type assertion: https://golang.org/ref/spec#Type_assertions
[9] одобрен: https://github.com/golang/go/wiki/vgo
[10] JetBrains GoLand 2018.2: https://habr.com/company/JetBrains/blog/419061/
[11] 1.11: https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more
[12] доступных контейнера три: https://golang.org/pkg/container/
[13] github.com/stretchr/testify: https://github.com/stretchr/testify
[14] esata: https://habr.com/users/esata/
[15] github.com/onsi/ginkgo: https://github.com/onsi/ginkgo
[16] Такой исключительный Go: https://habr.com/post/425145/
[17] Источник: https://habr.com/post/421259/?utm_campaign=421259
Нажмите здесь для печати.