Ретроспектива 3-х лет написания Go

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

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

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

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

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

1. Ненужный импорт и переменные

Go заставляет код быть минималистичным

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

import (
    "fmt"
    "os" //not used
)
func main() {
    fmt.Println("Hola")
}

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

imported and not used: "os"

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

Функция диапазона, используемая для итерации коллекций, возвращает два значения

Первый - это позиция записи в коллекции. Второе значение содержит само значение записи.

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, вы, вероятно, привыкли к перегрузке методов. В методах перегрузки в методе может быть несколько сигнатур. Что ж ... У Голанга нет перегрузки методов.

5. Что случилось с наследованием?

Наследования нет

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

6. А как насчет интерфейсов?

Есть интерфейсы

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

Почему? Потому что вы не указываете программно, что ваша структура реализует интерфейс (например, class A реализует интерфейс I). Ваша структура выполняет интерфейс, если в ней есть методы, перечисленные интерфейсом. Это легче понять на примере.

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. А как насчет конструкторов?

Вы можете пропустить набор атрибутов при создании экземпляра новой структуры

Здесь нет конструкторов, подобных тем, которые вы можете найти в любом объектно-ориентированном языке. Определение структуры во многом напоминает то, что используется в Си. Есть одна потенциальная проблема: вы можете пропустить атрибуты, установленные при создании экземпляра новой структуры. В следующем коде halfMessage1 и halfMessage2 имеют неустановленные атрибуты.

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 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 кажутся вам самыми странными?