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



Сразу скажу, что эта статья: мое личное, полностью субъективное мнение. Список ниже — только небольшая выдержка без каких-либо критериев выбора. Для ясности расскажу о себе: у меня около 20 лет опыта работы, я работал с C, C++, Java, Scala, Python, R (если смотреть на R как на язык).
Я нахожу Go легким в изучении. Наверное, благодаря четко определенному замыслу, который устраняет особенности, подразумевающие сложный синтаксис. Так или иначе, я начинаю список.

1. Нежелательное импортирование и лишние переменные


Go заставляет придерживаться минимализма. Это означает, что бесполезное импортирование и лишние переменные вызовут ошибку компиляции. Например:

import (
    "fmt"
    "os" //not used
)

func main() {
    fmt.Println("Hola")
}

Компилятор возвращает:

imported and not used: "os"

2. Итерация по коллекциям


Функция range, используемая при итерации по коллекции, возвращает два значения. Первое значение — это позиция элемента в коллекции. Второе значение — это значение самого элемента.

x := [4]string{"one","two","three","four"}
for i, entry := range(x) {
   fmt.Printf("Element at position %d is %s\n", i, entry)
}

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

x := [4]string{"one","two","three","four"}
for i, entry := range(x) {
  fmt.Printf("Element %s\n", entry)
}

И такой код вызовет ошибку компиляции:

i declared but not used

Или даже хуже, вы пропустите i. Вот так:

x := [4]string{"one","two","three","four"}
for entry := range(x) {
   fmt.Printf("Element %s\n", entry)
}

Это может запутать. В переменной возвращается позиция элемента, но можно ожидать его значение.

Element %!s(int=0)
Element %!s(int=1)
Element %!s(int=2)
Element %!s(int=3)

Как решить проблему? Нужно просто обозначить неиспользуемую переменную i вот так:

x := [4]string{"one","two","three","four"}
    for _, entry := range(x) {
       fmt.Printf("Element %s\n", entry)
    }

3. Видимость атрибутов


Атрибуты видимы, когда начинаются с заглавной буквы. Атрибут, который не начинается с заглавной буквы, не видим. Это просто. Но я постоянно забываю об этом и делаю глупые ошибки.

type Message struct {
 Text string // This is public
 text string // This is private
}

4. Что с перегрузкой методов?



Никакой перегрузки методов нет. Если вы пришли из мира Java, скорее всего вы применяли перегрузку методов. В Golang перегрузки методов нет.

5. А наследование?


Наследования тоже нет. Эту особенность можно обойти, как описано здесь. Но я не думаю, что это действительно наследование.

6. Интерфейсы в Go


В отличие от перегрузки методов и наследования, интерфейсы в Go есть. Вы можете определить их как набор из сигнатур методов. Но они странные в сравнении с интерфейсами в других языках. Почему? Потому что вы не указываете программно, что структура реализует интерфейс. Структура автоматически удовлетворяет интерфейсу, когда реализует перечисленные в интерфейсе методы. Это проще понять на примере:

package main
import (
    "fmt"
)

type Speaker interface {
    SayYourName() string
    SayHello(b Speaker) string
}

type HappySpeaker struct {}
func(hs HappySpeaker) SayYourName() string {
    return "Happy"
}

func(hs HappySpeaker) SayHello(b Speaker) string {
    return fmt.Sprintf("Hello %s!",b.SayYourName())
}

type AngrySpeaker struct {}
func(as AngrySpeaker) SayYourName() string {
    return "Angry"
}

func(as AngrySpeaker) SayHello(b Speaker) string {
    return fmt.Sprintf("I'm not going to say hello to %s!",b.SayYourName())
}

func main() {
    // We have two different structs
    happy := HappySpeaker{}
    angry := AngrySpeaker{}
    // they can say their names
    fmt.Println(happy.SayYourName())
    fmt.Println(angry.SayYourName())

    // But they are also speakers
    fmt.Println(happy.SayHello(angry))
    fmt.Println(angry.SayHello(happy))

    // This is also valid
    var mrSpeaker Speaker = happy
    fmt.Println(mrSpeaker.SayHello(angry))
}

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

7. Конструкторы


В Go нет конструкторов, подобных тем, которые вы найдете в объектно-ориентированном языке. Определение структуры на Go очень похоже на определение структуры в языке C. Но есть одна потенциальная проблема: вы можете пропустить инициализацию атрибутов. В коде ниже у halfMessage1 и halfMessage2 пустые атрибуты.

import (
    "fmt"
)

type Message struct {
    MsgA string
    MsgB string
}

func(m Message) SayIt() {
  fmt.Printf("[%s] - [%s]\n",m.MsgA, m.MsgB)
}

func main() {
    fullMessage1 := Message{"hello","bye"}
    fullMessage2 := Message{MsgA: "hello", MsgB: "bye"}
    halfMessage1 := Message{"hello",""}
    halfMessage2 := Message{MsgA: "hello"}
    emptyMessage := Message{}
    fullMessage1.SayIt()
    fullMessage2.SayIt()
    halfMessage1.SayIt()
    halfMessage2.SayIt()    
    emptyMessage.SayIt()        
}

Код выше выведет:

[hello] - [bye]
[hello] - [bye]
[hello] - []
[hello] - []
[] - [] 

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

package main

import (
    "fmt"
)

type Message struct {
    MsgA string
    MsgB string
}

func(m Message) SayIt() {
  fmt.Printf("[%s] - [%s]\n",m.MsgA, m.MsgB)
}

func NewMessage(msgA string, msgB string) *Message{
  if len(msgA) * len(msgB) == 0 {
     return nil
  }
 
  return &Message{MsgA: msgA, MsgB: msgB}
}

func main() {
   // A correct message
   msg1 := NewMessage("hello","bye")    
   if msg1 != nil {
      msg1.SayIt()
   } else {
      fmt.Println("There was an error")
   }
   // An incorrect message
   msg2 := NewMessage("","")
   if msg2 != nil {
      msg2.SayIt()
   } else {
      fmt.Println("There was an error")
   }
}

Заключение


Это была небольшая выборка особенностей, которые следует учитывать, когда вы программируете на Go. А что в нем показалось вам самым странным при программировании на Go?

image

Получить востребованную профессию с нуля или Level Up по навыкам и зарплате, можно пройдя онлайн-курсы SkillFactory: