Недавно я закончил читать книгу Тейвы Харсаньи "
100 ошибок и как их избежать", и вместо того, чтобы писать рецензию (всем, кто работает с Go, стоит ее прочитать), я решил поделиться четырьмя ошибками, которые показались мне интересными и о которых я раньше не знал.
#13. Создание пакетов утилит
Поэтому я упорно придерживаюсь принципа: в большинстве своих проектов я пишу пакеты утилит в тот момент, когда какой-либо фрагмент кода используется более одного раза. При этом я обычно называл пакет utils, если он выполнял несколько общих (в контексте API) операций, таких как форматирование, валидация и т.д. Из этой книги и советов других разработчиков я понял, что понятие util не имеет смысла. Его можно назвать common, shared или base, но это все равно останется бессмысленным названием, которое не описывает ничего, связанного с тем, что предоставляет API. Например,
package util
func NewStringSet(...string) map[string]struct{} { ... }
func SortStringSet(map[string]struct{}) []string { ... }
// client.go
set := util.NewStringSet("A", "B", "C")
fmt.Println(util.SortStringSet(set))
Вместо этого вспомогательный пакет следует разбить на части (если есть несколько конкурирующих обязанностей) и переименовать во что-то более выразительное и понятное, например, в stringset в данном случае. В дальнейшем, возможно, чисто из вредности, я предпочитаю структурировать свои пакеты по принципу «родитель (интерфейс | контекст) -> ребенок (реализация)», поэтому stringset можно найти по следующему пути: your_project/pkg/utils/stringset. Возможно, в следующей статье «Еще больше ошибок в Go» будет доказано, что это тоже плохой проект; а пока!
package stringset
func New(...string) map[string]struct{} { ... }
func Sort(map[string]struct{}) []string { ... }
#26. Утечка мощности слайса
Несмотря на то, что я работаю с Go в профессиональном контексте уже более двух лет, я до сих пор не удосужился изучить различия между слайсами и массивами. Я с уверенностью могу сказать, что в той или иной функции или блоке кода я не знаю, какие из них используются и какие лучше использовать. Поэтому, дойдя до этого раздела книги, где подробно описываются различные типы данных и связанные с ними типичные ошибки, мне пришлось провести небольшое исследование на тему "«Слайсы» и «Массивы»". Эндрю Герранд в книге "
Go Slices: usage and internals" описывает слайсы и массивы следующим образом:
Тип «слайс» — это абстракция, построенная поверх типа массива в Go, поэтому для понимания слайсов необходимо сначала разобраться с массивами. В определении типа массива указывается длина и тип элемента.
Например, тип [4]int представляет собой массив из четырех целых чисел. Размер массива фиксирован, а длина является частью его типа ([4]int и [5]int — разные, несовместимые типы). Массивы можно индексировать обычным способом, поэтому выражение s[n] открывает доступ к n-му элементу, начиная с нуля. Массивы имеют свое место, но они несколько негибки, поэтому в коде на Go они встречаются нечасто. А вот срезы встречаются повсеместно. Они строятся на основе массивов и обеспечивают большую мощность и удобство. Спецификация типа для слайса — []T, где T — тип элементов слайса. В отличие от типа массива, тип слайса не имеет определенной длины.
Итак, немного во всём этом разобравшись, давайте уточним, как это относится к leak capacity?
Ссылаясь на исходный текст из репозитория "
Github: teivah/100-go-mistakes", Тейва объясняет,
Операция слайсинга над msg с использованием msg[:5] создает слайс длиной пять. Однако его емкость остается такой же, как и у исходного фрагмента. Оставшиеся элементы все равно выделяются в памяти, даже если в конечном итоге на msg не ссылаются
func consumeMessages() {
for {
msg := receiveMessage()
// Что-то делаем с сообщением
storeMessageType(getMessageType(msg))
}
}
func getMessageType(msg []byte) []byte {
return msg[:5]
}
func receiveMessage() []byte {
return make([]byte, 1_000_000)
}
func storeMessageType([]byte) {}
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d KB\n", m.Alloc/1024)
}
Далее, чтобы пояснить проблему в более широком масштабе, Тейва приводит следующий сценарий:
Рассмотрим пример с сообщением большой длины — 1 млн. байт. После операции слайсинга базовый массив все еще содержит 1 млн. байт. Следовательно, если мы храним в памяти 1000 сообщений, то вместо 5 КБ мы храним около 1 ГБ.
Рекомендуемый подход для решения этой проблемы? Используйте копирование слайсов!
func getMessageTypeWithCopy(msg []byte) []byte {
msgType := make([]byte, 5)
copy(msgType, msg)
return msgType
}
#46. Использование имени файла в качестве ввода функции
Даже в облачно-ориентированной среде Kubernetes я сталкиваюсь с написанием функций, очень похожих на пример Теивы. Простая функция, которая считывает содержимое файла с любым именем, указанным в качестве аргумента. На самом деле, я собирался объяснить некоторые из своих разочарований по поводу проекта, но автор так хорошо объяснил один из самых больших подводных камней в этом отрывке,
При создании новой функции, которая должна читать файл, передача имени файла не считается лучшей практикой и может иметь негативные последствия, например, усложнить написание модульных тестов.
func countEmptyLinesInFile(filename string) (int, error) {
file, err := os.Open(filename)
if err != nil {
return 0, err
}
// Обрабатываем замыкание файла
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// ...
}
return 0, nil
}
При написании модульных тестов я никогда не любил размещать тестовые ресурсы в репозитории, тогда как прагматично их можно было бы хранить внутри самих тестов. Возможно, это упрямство, но я считаю, что лучше хранить все ресурсы, необходимые для тестового случая, внутри testCase.
Итак, какой проект лучше для таких API? Сделать так, чтобы функция принимала аргумент io.Reader! Прочитав это объяснение, я на самом деле почувствовал облегчение от того, насколько это лучший проект!
func countEmptyLines(reader io.Reader) (int, error) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
// ...
}
}
#54. Отсутствие обработки ошибок отсрочки
defer — это часто используемое ключевое слово в Go, однако я никогда не слышал об ошибках с отложенным типом. На самом деле, если бы вы спросили меня до прочтения этой главы, я бы не поверил, что можно использовать тип ошибки с defer. Это распространенная ошибка, поскольку в десятках и десятках кодовых баз я видел паттерны, которые не справляются с ошибками defer. Итак, как мы можем улучшить ситуацию? Используя приведенный пример,
func getBalance(db *sql.DB, clientID string) (
float32, error) {
rows, err := db.Query(query, clientID)
if err != nil {
return 0, err
}
defer rows.Close()
// Используем строки
}
Интерфейс Closer, реализованный в примере типом *sql.Rows, включает функцию closer.Closer, которая возвращает ошибку. При этом, как видно из приведенного примера, никакой обработки возвращаемого значения ошибки не происходит. Однако если вызов функции defer подразумевает набор логических действий, которые должны произойти до возврата из функции, как обработать ошибку, не потеряв контекст и не потеряв уже существующую ошибку?
func getBalance3(db *sql.DB, clientID string) (balance float32, err error) {
rows, err := db.Query(query, clientID)
if err != nil {
return 0, err
}
defer func() {
closeErr := rows.Close()
if err != nil {
if closeErr != nil {
log.Printf("failed to close rows: %v", err)
}
return
}
err = closeErr
}()
// Используем строки
return 0, nil
}
Ошибка rows.Close присваивается другой переменной: closeErr. Прежде чем присвоить ее переменной err, мы проверяем, отличается ли err от nil. Если это так, то ошибка уже была возвращена функцией getBalance, поэтому мы решаем записать err в лог и вернуть существующую ошибку.
Мне показалось интересным объяснение проекта, поскольку он включает в себя не очень распространенную особенность языка go, называемую возвратами. Я слышал, что эта особенность является антипаттерном для простого дизайна, или запахом кода, которого следует избегать, но у меня самого нет реального мнения, хотя у меня никогда не было необходимости использовать ее, так что здесь я могу признать свою неопытность. Вместо этого я сошлюсь на "
GeeksforGeeks: Именованные параметры возврата в Golang".
В Golang именованные возвратные параметры принято называть именованными параметрами. Golang позволяет присваивать имена возвращаемым или результирующим параметрам функций в сигнатуре или определении функции. Или можно сказать, что это явное именование возвращаемых переменных в определении функции.
Таким образом, вместо того чтобы возвращать nil в самом конце функции, она будет возвращать значение err после выполнения функции defer func(). При этом значение err будет обновлено статусом этой операции, если не существует другого значения err.
P.S.
Также обращаем ваше внимание на то, что у нас на сайте проходит
распродажа.