Источник: Nuances of Programming
Я использую 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",
},
}
Спорим, разницу вы не заметите!
Читайте также:
Перевод статьи Andrew Hayes: 8 coding hacks for Go that I wish I’d known when I started