Пять вещей, которые вы можете ненавидеть в Go

задняя часть Go

В последние годы Go появился среди новых языков программирования. Однако кажется неуместным называть Go «новичком», поскольку Google запустил Go еще в 2009 году и выпустил первую финальную версию (Go 1.0) в 2012 году. К настоящему времени Go вырос до версии 1.10, которая впечатляет и продолжает добавлять новые функции.

Почему это называется эгоистичным (самомадушевым) ...

Как мы все знаем, пойти любит «пойти на свой путь», когда речь идет о внедрении или синтаксисе. На английском эта ситуация описывается как «самостоятельно праведно». Многие концепции от других языков программирования не существуют в ходу, или если они делают, их поведение становится «неузнаваемой». Последний случай может привести к неожиданным ошибкам и даже смущать разработчиков.

Строгий синтаксис Go часто утомляет разработчиков. Компилятор Go не разрешает неиспользуемый импорт и переменные и делает все возможное, чтобы заблокировать их, даже не помещая фигурные скобки в новую строку. Go обеспечивает относительно фиксированный и почти единообразный стиль программирования. Всякий раз, когда компилятору Go что-то не нравится, это заканчивается ошибкой компиляции.

Go обеспечивает очень строгую безопасность типов. Будучи настолько строгим, мы даже можем использовать его для достижения некоторых специальных эффектов и ошибок программирования, некоторые из которых мы обсудим позже в статье. Однако нам редко требуется явно объявлять типы в Go, поскольку типы часто можно получить путем присваивания, также известного как вывод типа.

Я не пытаюсь предоставить вопросы и ответы!

Я начал использовать Go Highty на работе более года назад. Go вряд ли мой любимый язык программирования, но я признаю, что Иди сыграл роль в улучшении производительности развития. На самом деле я сделал несколько небольших проектов, используя Go, в основном встроенные приложения. Перекрестные возможности перекрестной платформы Toolchain (скомпилированы для других операционных систем или платформ процессора) великолепны и уже впереди его конкурентов.

Теперь давайте взглянем на некоторые особенности Go. Начать работать с Go на самом деле довольно просто, на изучение основ могут уйти всего выходные. Но когда вы начинаете делать с Go какие-то более сложные вещи, начинают всплывать всевозможные странные вещи.

Иногда эти функции настолько странные, что Google дает ответы на вопросы типа «почему X ведет себя так или иначе». Go во многом отличается от других языков, и кажется, что программисты в какой-то момент обязательно наткнутся на некоторые ловушки. Канал gopher Slack подтвердил, что это так, с таким описанием: «Теперь вам действительно следует хорошенько взглянуть на Go, потому что каждый разработчик задает этот вопрос в своей карьере Go». Часто наша интуиция не соответствует возможностям Go. Например, в варианте Google C общедоступные типы, функции, константы и т. д. начинаются с прописной буквы, чтобы указать, что они общедоступны, а идентификаторы начинаются со строчной буквы, чтобы указать, что они являются частными.

Тем не менее, многие решения о Go утверждаются после длительных обсуждений в списках рассылки или документах с предложениями. Однако обсуждаемые варианты использования настолько специфичны, что многие разработчики до сих пор точно не знают, как это относится к проблеме, которую они пытаются решить.

Лично мне больше всего нравится то, что Go не предоставляет реентерабельных блокировок, т. е. блокировок, которые могут быть рекурсивно получены одним и тем же потоком или Горутиной (вариантами Корутины или Зеленой нити). Невозможно реализовать такую ​​функцию без взлома, потому что потоки в Go недоступны, а горутины не предоставляют идентификатор, который можно использовать для рекурсивной идентификации той же самой корутины.

В этом посте я хочу представить пять особенностей Go и его синтаксиса, все из которых довольно неясны.

1. Безумная тень

Начнем с самого простого: каждый хороший разработчик слышал о Shadowing, и обычно это происходит в контексте переменных. Вот простой пример всего с двумя областями:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__foo("foo")
func foo(var1 string) {
  for {
    var1 := "bar"
    fmt.Println(var1)
    break
  }
__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Мы создаем переменную с нотацией присваивания := и выводим тип переменной из присвоенного значения (ссылка на тип). Здесь это строка. Поэтому мы создаем переменную с тем же именем, что и параметр функции во внутренней области видимости (цикл for). Мы затеняем входной параметр и выводим «bar».

Все идет нормально. Однако в Go необходимо указывать имена пакетов для свойств других пакетов (т. е. структур, методов, функций и т. д.), которые можно увидеть в пакете fmt, предоставляющем функцию Println.

Итак, давайте немного рефакторим предыдущий пример:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__foo("foo")
func foo(var1 string) {
  for {
    fmt := "bar"
    fmt.Println(var1)
    break
  }
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

На этот раз мы получили ошибку компиляции и попытались вызвать функцию Println для строки. Но это не всегда так очевидно. Даже несколько строк кода могут преподнести нам «сюрприз», когда код внезапно перестанет компилироваться.

Если структуры перекрываются, это может быть проблематично. Возьмем странный пример:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type task struct {
}
  
func main() {
  task := &task{}
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Мы создаем структуру с именем task и ее экземпляр. Мы намеренно используем в качестве имени структуры задачу в нижнем регистре, поскольку, как упоминалось ранее, Go использует первую букву для определения видимости, поэтому задача здесь является закрытой.

Пока все выглядит хорошо, Go компилирует созданную нами задачу. Однако все внезапно изменилось, когда мы попытались добавить еще одну строку кода.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type task struct {
}
  
func main() {
  task := &task{}
  task = &task{}
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Теперь он не компилируется и показывает, что задача не является типом. На данный момент Go не может отличить типы от переменных. Можно сказать, что в JavaScript переменная task может быть ссылкой на тип, но в Go это невозможно, потому что типы нельзя присваивать переменным как значения.

Теперь вопрос: трагедия ли это? Обычно нет, но это часто происходит без моего ведома. Позже может быть какой-то код, который пытается получить доступ к структуре или пакету с тем же именем, и каждый раз поиск проблемы занимает несколько минут.

Говоря о проблемах с типом, давайте рассмотрим другой пример.

2. Введите или не введите, вот в чем вопрос!

Мы уже знаем, как создавать структуры и функции. Иногда мы иногда «переименовываем» тип, например:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type handle int__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Это создаст тип с именем handle, который ведет себя как int. Часто эту функцию называют алиасингом типов. Возможно, вы тоже подумали об этой функции, но только не в Go. Однако, начиная с Go 1.9, эта функция полностью поддерживается.

Давайте посмотрим, какие забавные вещи мы можем делать с Go:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type handle int
  
func main() {
  var var1 int = 1
  var var2 handle = 2
  types(var1)
  types(var2)
}
  
func types(val interface{}) {
  switch v := val.(type) {
  case int:
    fmt.Println(fmt.Sprintf("I am an int: %d", v))
  case handle:
    fmt.Println(fmt.Sprintf("I am an handle: %d", v))
  }
}

I am an int: 1
I am an handle: 2__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

В этом примере мы используем пару действительно крутых функций Go. Оператор switch-type-case — это тип сопоставления с шаблоном, аналогичный instanceof в Java или typeof в JavaScript. Мы приравниваем interface{} к Object в Java, потому что это пустой интерфейс, который автоматически реализуется каждым классом Go.

Интересно, что разработчики Java хотели, чтобы handle тоже был int, чтобы это соответствовало первому случаю. Но это не так, потому что в Go не работает наследование типов в объектной ориентации.

Другая возможность состоит в том, что handle является псевдонимом для int, как typedef в C/C++, но это тоже не так. Компилятор Go создает новый TypeSpec, так сказать, клон исходного типа. Поэтому они полностью независимы.

Однако, начиная с Go 1.9, поддерживаются псевдонимы истинного типа. Пример ниже лишь немного изменен.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type handle = int
  
func main() {
  var var1 int = 1
  var var2 handle = 2
  types(var1)
  types(var2)
}
  
func types(val interface{}) {
  switch v := val.(type) {
  case int:
    fmt.Println(fmt.Sprintf("I am an int: %d", v))
  }
  switch v := val.(type) {
  case handle:
    fmt.Println(fmt.Sprintf("I am an handle: %d", v))
  }
}

I am an int: 1
I am an int: 2
I am an handle: 1
I am an handle: 2__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Вы заметили разницу? На самом деле, вместо того, чтобы использовать тип handle int сейчас, мы используем type handle=int, чтобы создать дополнительное имя (псевдоним) для int, handle. Это означает, что оператор switch также должен быть изменен, потому что на этот раз int и handle являются одним и тем же типом для компилятора, и если у вас нет другого двойного регистра, вы получите ошибку компиляции. Поскольку псевдонимы типов были введены в Go 1.9, многие люди могут подумать, что приведенные выше клоны типов являются псевдонимами классов.

В целях демонстрации давайте определим тип Callable, который состоит из простой функции без параметров и без возвращаемого значения.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type Callable func()__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Теперь создайте соответствующую функцию.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__func main() {
  myCallable := func() {
    fmt.Println("callable")
  }
  test(myCallable)
}
  
func test(callable Callable) {
  callable()
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Смотри, это просто. Благодаря механизму вывода типов Go компилятор автоматически распознает, что myCallable должен соответствовать сигнатуре функции Callable. Таким образом, компилятор может неявно преобразовать myCallable в Callable. Затем myCallable передается тестовой функции. Это одно из немногих исключений, когда выполняются неявные преобразования, и, как правило, все формы преобразования должны указываться явно.

Теперь мы подошли к моменту, когда нам нужно использовать Reflection. Как и другие языки, Reflection предоставляет возможность анализировать или изменять поведение во время выполнения. Информация о типе часто используется для изменения поведения во время выполнения в зависимости от типа данных значения.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type Callable func()
  
func main() {
  callable1 := func() {
    fmt.Println("callable1")
  }
  
  var callable2 Callable
  callable2 = func() {
    fmt.Println("callable2")
  }
  
  test(callable1)
  test(callable2)
}
  
func test(val interface{}) {
  switch v := val.(type) {
  case func():
    v()
  default:
    fmt.Println("wrong type")
  }
}

callable1
wrong type__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

callable1 теперь имеет тип функции func(), а callable2 явно объявлен как Callable. Callable — это отдельный TypeSpec и, следовательно, не тот же тип, что и func(). Оба случая теперь должны обрабатываться отдельно нашим обработчиком Reflection. Однако эти проблемы можно решить с помощью псевдонимов типов, представленных в Go 1.9.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type Callable=func()__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

3. Лень - характер суслика!

Симпатичный логотип языка Go, суслик ленив по натуре, и выбор этого логотипа тоже имеет определенное репрезентативное значение.

Одной из моих любимых функций Go является Lazy Evaluation, то есть ленивое выполнение кода. Поскольку Java представила Stream API, разработчики Java также знали об этой функции.

Давайте взглянем на следующий фрагмент кода:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__func main() {
  functions := make([]func(), 3)
    for i := 0; i < 3; i++ {
      functions[i] = func() {
      fmt.Println(fmt.Sprintf("iterator value: %d", i))
      }
    }
  
  functions[0]()
  functions[1]()
  functions[2]()
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Вот массив из трех элементов, цикл и замыкание, и что получится в результате?

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__iterator value: 3
iterator value: 3
iterator value: 3__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Мы могли бы подумать, что это 0,1,2, но на самом деле это 3,3,3. Верно!

В других языках программирования, таких как Java, значение переменной фиксируется при создании замыкания, тогда как Go фиксирует только указатель на саму переменную. Проблема в том, что во время итерации значение переменной постоянно меняется. После завершения цикла мы выполняем замыкание и видим только последнее значение. Такое поведение понятно, поскольку мы знаем, что у нас есть только указатели, но на самом деле оно не очень интуитивно понятно.

Если мы хотим сохранить это значение, нам нужно знать, как это значение вычисляется при создании замыкания.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__func main() {
  functions := make([]func(), 3)
  for i := 0; i < 3; i++ {
    functions[i] = func(y int) func() {
      return func() {
        fmt.Println(fmt.Sprintf("iterator value: %d", y))
      }
    }(i)
  }
  
  functions[0]()
  functions[1]()
  functions[2]()
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Мы создали временную функцию, которая принимает переменную в качестве параметра и возвращает замыкание. Мы вызываем эту функцию немедленно. Поскольку значение переменной должно быть вычислено при вызове внешней функции, внутреннее замыкание может зафиксировать правильное значение. То, что мы получаем, это 0,1,2.

Незадолго до написания этой статьи я нашел еще один способ. Мы можем создать переменную с таким же именем внутри цикла и присвоить ей фактическое значение. Это также захватывает значение переменной, поскольку этот метод создает новую переменную (следовательно, новый указатель) на каждой итерации цикла.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__func main() {
  functions := make([]func(), 3)
  for i := 0; i < 3; i++ {
    i := i // Trick mit neuer Variable
    functions[i] = func() {
      fmt.Println(fmt.Sprintf("iterator value: %d", i))
    }
  }
  
  functions[0]()
  functions[1]()
  functions[2]()
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Ленивое вычисление часто является интересной темой с точки зрения скорости выполнения. В конце концов, я могу создавать замыкания, не используя его. Если это так, то зачем запрашивать значение? Это также очень неинтуитивно, на мой взгляд.

4. Все мы немного похожи на сусликов?

Мы уже знаем, что interface{} в Go похож на Object в Java — каждый тип в Go автоматически реализует этот пустой интерфейс. Однако автоматическая реализация интерфейсов применима не только к пустым интерфейсам: каждая структура или тип, реализующий все методы интерфейса, также будет автоматически реализовывать интерфейс.

Чтобы лучше проиллюстрировать проблему, давайте рассмотрим следующий пример:

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type Sortable interface {
  Sort(other Sortable)
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Структуры, определяющие этот метод, автоматически сортируются.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

За исключением синтаксиса типа получателя, который используется для привязки функции к типу (в данном случае к структуре), мы реализовали все методы интерфейса Sortable. Теперь мы Sortable!

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__var sortable Sortable = &MyStruct{}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

На первый взгляд автореализуемые интерфейсы могут показаться полезными, но они могут все усложнить, особенно в больших приложениях, где может возникнуть путаница, если несколько интерфейсов имеют одни и те же методы. Какой интерфейс на самом деле хочет реализовать разработчик? Может быть, они должны прояснить это в комментариях к коду!

В Go также есть решение для обеспечения того, чтобы тип реализовывал интерфейс, например, ключевое слово Implements в Java, что слишком просто.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}
var _ Sortable = MyStruct{}
var _ Sortable = (*MyStruct)(nil)__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

5. ноль и ничего

Теперь мы все знаем, что есть большая разница между «нулевым» и «нулевым», но, возможно, не все знают, что «ничего» не всегда означает «ничего». Чтобы продемонстрировать это, мы определяем собственный тип ошибки (исключение).

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__type MyError string
func (m MyError) Error() string {
  return string(m)
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Мы создаем новый тип, который клонируется из строкового типа. Нам просто нужно сообщение об ошибке, так что этого достаточно. Для реализации интерфейса ошибок (да, строчных, по идее он не должен быть публичным, но Go может делать что угодно) нужно реализовать метод Error.

Далее нам нужна еще одна функция, которая всегда возвращает Nil.

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__func test(v bool) error {
  var e *MyError = nil
  if v {
    return nil
  }
  return e
}__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

Эта функция всегда возвращает nil независимо от того, передаем ли мы значение true или false, верно?

__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__func main() {
  fmt.Println(nil == test(true))
  fmt.Println(nil == test(false))
}

true
false__Thu Aug 02 2018 16:33:05 GMT+0800 (CST)____Thu Aug 02 2018 16:33:05 GMT+0800 (CST)__

При возврате e указатель *MyError указывает на экземпляр ошибки интерфейса, который не равен нулю! Это логично? Когда вы знаете, как интерфейсы представлены в Go, вы понимаете, что это логично.

Внутри Go интерфейс представляет собой структуру, которая содержит фактический целевой экземпляр (здесь nil) и тип интерфейса (здесь ошибка), и, согласно спецификации языка Go, только когда оба значения этой структуры равны nil, экземпляр интерфейса равен нулю. Поэтому, если вы действительно хотите вернуть nil, сделайте это явно.

Что особенного

Также стоит упомянуть, что, как упоминалось ранее, Go определяет видимость типов и функций на основе имен. Функция или тип являются открытыми, если первая буква в верхнем регистре (например, Foo), и закрытыми, если первая буква в нижнем регистре (например, foo). Однако в Java есть private, а в Go только package-private.

В общем, мы можем использовать это правило видимости, отличное от camelCase в Go, будь то функции, структуры или константы, но наша IDE имеет подсветку синтаксиса, так что кого это волнует!

Интересно, что Go поддерживает идентификаторы Unicode. Таким образом, японский язык (Nihongo — это японский язык) является вполне законным идентификатором, но обычно считается частным. Зачем? Потому что в японских иероглифах нет заглавных букв.

Привет от "GO Sla"

В некотором смысле Go — очень уникальный язык. В своей повседневной работе вы можете получать удовольствие от Go. Если вы уже знаете подводные камни, которые мы здесь упомянули (и многие другие), то разработка даже самых больших приложений не должна вызвать проблем. Тем не менее, есть постоянные напоминания о том, что с языком что-то не так.

За последние годы в Go многое произошло, и, помимо добавления новых функций, в Go 2 есть множество областей, требующих улучшения, включая некоторые несоответствия в синтаксисе и поведении во время выполнения. Однако дата запуска Go 2 неизвестна, и нет четкой дорожной карты.

Если вы хотите использовать Go, используйте его, несмотря на подводные камни. Однако будьте готовы к этому: иногда вы можете запутаться и нуждаться в длительной отладке или устранении неполадок, прочитав FAQ или посетив канал Gopher Slack.

Оригинальная ссылка:JA под enter.com/5-things-yo…

благодарныйЧжан ЧанОбзор этой статьи.