Я использую 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",
},
}
Спорим, разницу вы не заметите🙂!
Читайте также:
- Реализация интерфейсов в Golang
- Бенчмарки в Golang: тестируем производительность кода
- Оптимизация структур в Golang для эффективного распределения памяти
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Andrew Hayes: 8 coding hacks for Go that I wish I’d known when I started