8 хитрых приемов написания кода на Go

Я использую Go уже несколько лет, и иногда удается обнаружить маленькие хитрости в написании кода, которые облегчают мне жизнь. Сегодня я поделюсь ими с вами!

1. Проверка наличия ключа в map

Этот прием наверняка многие уже знают, но я так часто его применяю, что просто не могу о нем не упомянуть. Чтобы проверить, есть ли ключ в map, просто вызываете:

_, keyIsInMap := myMap["keyToCheck"]
if !keyIsInMap {
  fmt.Println("key not in map")
}

2. Проверка при приведении типов переменной

Иногда нужно провести преобразование переменных из одного типа в другой. Проблема в том, что в случае неверного типа код запаникует. Например, следующий код пытается привести переменную data к строковому типу string:

value := data.(string)

Здесь преобразование data в тип string не произойдет, поэтому код запаникует. Но есть способ лучше! Аналогично проверке наличия ключа в map: при приведении типов получаем логическое значение и проверяем, произошло приведение или нет:

value, ok := data.(string)

В этом примере ok  —  логическое значение, которое сообщает, было ли приведение типов успешным или нет. Таким образом работа с несоответствием типов ведется более изящно, чем при механизме паники.

3. Указание размера массива при использовании append

Для добавления элементов в массив лучше всего задействовать append. Например:

for _, v := range inputArray {
  myArray = append(myArray, v)
}

Однако в случае больших массивов процесс добавления замедлится, потому что append потребуется постоянно увеличивать размер myArray для новых значений. Лучше сначала указать длину массива, а затем присвоить каждое значение напрямую:

myArray := make([]int, len(inputArray))
 for i, v := range inputArray {
  myArray[i] = v
 }
}

Есть и третий вариант, который мне нравится еще больше: он сочетает два предыдущих! Считаю его чуть более удобным для восприятия, к тому же он не приводит к потери скорости, ведь размер назначается вначале:

myArray := make([]int, 0, len(inputArray))
for _, v := range inputArray {
  myArray = append(myArray, v)
}

Здесь размер массива устанавливается равным 0, а максимальный размер задается равным длине входного массива. Поэтому append не потребуется менять размер на ходу. При сравнении времени трех вариантов на массиве из 100 миллионов целых чисел разница в скорости очевидна:

normal array append took   3782.1423ms
presized array took        549.8333ms
presized array append took 685.9402ms

4. Использование append и многоточия для объединения массивов

Иногда бывает нужно объединить два массива. И тогда очень кстати, что append  —  это функция с переменным числом аргументов. Посмотрите, как выглядит обычный вызов append:

myArray = append(myArray, value1)

И append позволяет добавлять несколько элементов одновременно:

myArray = append(myArray, value1, value2)

Но самое крутое  —  это расширение массива с помощью  при передаче его в функцию. Итак, объединяем массив inputArray с массивом myArray:

myArray = append(myArray, inputArray...)

При этом происходит увеличение количества значений массива inputArray и передача их в append.

5. Отображение имен и значений параметров при выводе

Осваивать этот прием пришлось очень долго, зато теперь я все время им пользуюсь. Раньше для отображения имен и значений параметров в структуре я выполнял маршалинг в JSON и логировал это. Но есть гораздо более простой способ: при выполнении Printf добавлять + в формат. Пример:

fmt.Printf("%+v \n", structToDisplay)

Для получения такого же вывода на Go надо поменять в его синтаксическом представлении + на #:

fmt.Printf("%#v \n", structToDisplay)

Сравнение разных выводов:

Without params:       {first value 2}
With params:          {Value1:first value Value2:2}
As go representation: main.MyStruct{Value1:"first value", Value2:2}

6. Задействование iota с пользовательскими типами при перечислении

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

type PossibleStates int

const (
 State1 PossibleStates = iota
 State2
 State3
)

func UpdateState(newState PossibleStates) error {

Здесь создается пользовательский тип PossibleStates («возможные состояния»), после чего каждое перечисление будет иметь тип PossibleState, значение которого присваивается ключевым словом iota. Затем, когда кто-то вызывает updateState, компилятор гарантирует отправку только этих possible states, а не прежних int.

7. Использование в качестве параметров (при создании имитированного интерфейса) функций, соответствующих интерфейсным функциям

Этот прием стал для меня откровением. Допустим, имеется интерфейс, который надо сымитировать:

type DataPersistence interface {
 SaveData(string, string) error
 GetData(string) (string, error)
}

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

type MockDataPersistence struct {
 SaveDataFunc func(string, string) error
 GetDataFunc func(string) (string, error)
}

// SaveData просто вызывает параметр SaveDataFunc
func (mdp MockDataPersistence) SaveData(key, value string) error {
 return mdp.SaveDataFunc(key, value)
}

// GetData просто вызывает параметр GetDataFunc
func (mdp MockDataPersistence) GetData(key string) (string, error) {
 return mdp.GetDataFunc(key)
}

Это означает, что при тестировании функции настраиваются, как нам надо, прямо в этом же тесте:

func TestMyStuff(t *testing.T) {
 mockPersistor := MockDataPersistence{}
 // здесь устанавливаем SaveData, чтобы просто вернуть ошибку
 mockPersistor.SaveDataFunc = func(key, value string) error {
  return fmt.Errorf("error to check how your code handles an error")
 }

// теперь проверяем, как thingToTest (то, что тестируется) разбирается с тем, когда 
 // SaveData возвращает ошибку
 err := thingToTest(mockPersistor)
 assert.Nil(t, err)
}

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

8. Создание собственного интерфейса в случае его отсутствия

Допустим, вы используете другую библиотеку Go, и там есть структура, но интерфейса из нее не сделано  —  создайте его сами. Вот, например, эта структура:

type OtherLibsStruct struct {}

func (ols OtherLibsStruct) DoCoolStuff(input string) error {
 return nil
}

Прямо в коде создаем интерфейс, который ее реализует:

type InterfaceForOtherLibsStruct interface {
 DoCoolStuff(string) error
}

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

Бонус: инстанцирование вложенных анонимных структур

А этот прием мне приходилось задействовать несколько раз при использовании сгенерированного кода. Иногда при генерировании кода получается вложенная анонимная структура. Пример:

type GeneratedStuct struct {
  Value1 string `json:"value1"`
  Value2 int `json:"value2"`
  Value3 *struct {
    NestedValue1 string `json:"NestedValue1"`
    NestedValue2 string `json:"NestedValue2"`
  } `json:"value3,ommitempty"`
}

Допустим, теперь надо создать экземпляр этой структуры для использования. Как это сделать? С Value1 и Value2 все просто, но как инстанцировать указатель на анонимную структуру (Value3)? Мое первое решение: написать его в JSON, а затем маршалировать в структуру. Но это ужасно и как-то по-дилетантски. Оказывается, нужно использовать другую анонимную структуру при ее инстанцировании:

myGeneratedStruct := GeneratedStuct{
  Value3: &struct {
   NestedValue1 string `json:"NestedValue1"`
   NestedValue2 string `json:"NestedValue2"`
  }{
   NestedValue1: "foo",
   NestedValue2: "bar",
  },
 }

Это очевидно, но имейте в виду, что она должна точно соответствовать, вплоть до тегов JSON. И хотя все это будет работать, но из-за несоответствия типов не удастся скомпилировать следующее:

myGeneratedStruct := GeneratedStuct{
  Value3: &struct {
   NestedValue1 string `json:"nestedValue1"`
   NestedValue2 string `json:"nestedValue2"`
  }{
   NestedValue1: "foo",
   NestedValue2: "bar",
  },
 }

Спорим, разницу вы не заметите🙂!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Andrew Hayes: 8 coding hacks for Go that I wish I’d known when I started

Предыдущая статьяИИ: решение неверно поставленных задач
Следующая статьяКак очистить код с помощью SASS