Лучшие практики Golang: путь к чистому коду
В статье познакомимся с рекомендациями по написанию чистого кода на Go. Разберемся на примерах с особенностями языка и применим на практике основные синтаксические конструкции. Make и new – это встроенные механизмы для выделения памяти. Они используются в разных ситуациях и имеют свои особенности. Слайс — это массив переменной длины, который может хранить элементы одного типа. Внутренне представляет собой ссылку на базовый массив. При работе со слайсами часто возникает задача их «перенарезки» на более мелкие. В итоге получившийся слайс будет ссылаться на массив исходного. Об этом не стоит забывать, иначе в программе может возникнуть непредсказуемое потребление памяти. Рассмотрим эту особенность на конкретных примерах: Для предотвращения возникшей ошибки следует удостовериться, что копирование производится из временного слайса: Библиотека Go разработчика Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика» Библиотека Go для собеса Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса» Библиотека задач по Go Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go» Функции в языке Go могут возвращать несколько значений. Это называется «множественным возвратом». Данная особенность языка позволяет возвращать не только результат, но и дополнительные значения, такие как ошибки или другие необходимые данные. Пример объявления функции с множественным возвратом в Go: В приведенном примере функция Можно также игнорировать одно или несколько возвращаемых значений, используя пустой идентификатор ( Функции с множественным возвратом особенно полезны, когда требуется возвращать несколько результатов, например, при работе с ошибками или при параллельной обработке данных. Приведенная ниже функция В Go интерфейсы представляют собой набор методов, определяющих поведение объекта. Они позволяют абстрагироваться от конкретной реализации и работать с различными типами данных. То есть интерфейсы лишь определяют некоторый функционал, но сами его не реализуют. 💡Запомните важное правило Не стоит определять интерфейсы до их использования. Без реального примера сложно понять, действительно ли они необходимы, не говоря уже о методах, которые должны в них содержаться. Ниже представлен пример неправильного подхода при работе с интерфейсами: Верное решение с точки зрения Go — вернуть конкретный тип и позволить Горутины дешевы в запуске и эксплуатации, но у них есть конечная стоимость с точки зрения занимаемой памяти – вы не можете создать их бесконечное количество. В отличие от переменных, среда выполнения Go не может обнаружить, что горутина больше никогда не будет использоваться. 💡 Запомните важное правило Каждый раз, когда вы используете ключевое слово Если вы не знаете ответа на два приведённых вопроса, это может привести к возникновению утечек памяти. Обратимся к примеру для иллюстрации данной ошибки: Здесь функция Инженеры из Uber, которые принимают активное участие в развитии Go, создали детектор утечек горутин – пакет goleak, нацеленный на интеграцию с модульными тестами. Рассмотрим пример работы с этим инструментом на практике. Пусть есть некая функция И тест этой функции: При запуске тестов появляется сообщение об ошибке Этот инструмент может быть полезен при создании программ, так как позволяет сократить время на нахождение и устранение утечек памяти. Ошибки в Go представлены интерфейсом error, который определяет метод Игнорирование ошибок может привести к неопределенному поведению и усложнить отладку кода. Рассмотрим правильный способ обработки ошибок на примере работы с файлом: Классический способ сообщить об ошибке – вернуть тип Ниже представлен пример простой функции с паникой: При возникновении паники функция завершается и происходит запуск оставшихся отложенных функций с помощью defer, а также раскручивание стека горутин. В реальных условиях разработки следует избегать подобных ситуаций, так как это ставит под угрозу бесперебойную работу программы. К счастью, авторы Go предусмотрели этот недостаток и создали механизм восстановления после паники – Чтобы продемонстрировать работу данного механизма, обратимся к примеру: В результате выполнения кода мы получим следующий вывод: Заметьте, что функция Важно помнить, что качество и чистота кода зависят не только от языка программирования, но и от навыков разработчика. Использование рассмотренных примеров и следование общим принципам помогут улучшить качество создаваемого программного обеспечения. Хочется верить, что статья вдохновит читателей применять описанные практики в разработке на Go и создавать программы, в которых будет нетрудно разобраться даже новичку. И помните, чистый код – это путь к успешному проекту!Работа с данными
Отличие make и new
a := new(chan int) // a имеет тип *chan int b := make(chan int) // b имеет тип chan int
Скрытые данные в слайсах
// Плохая практика - непредсказуемое потребление памяти func cutSlice() []byte { slice := make([]byte, 256) fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 <0x...> return slice[:10] } func main() { res := cutSlice() fmt.Println(len(res), cap(res), &res[0]) // 10 256 <0x...> }
// Хорошая практика - данные скопированы из временного слайса func cutSlice() []byte { slice := make([]byte, 256) fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 <0x...> copyOfSlice := make([]byte, 10) copy(copyOfSlice, slice[:10]) return slice[:10] } func main() { res := cutSlice() fmt.Println(len(res), cap(res), &res[0]) // 10 256 <0x...> }
Функции
Функции с множественным возвратом
package main import "fmt" func swap(a, b int) (int, int) { return b, a } func main() { x, y := swap(1, 2) fmt.Println(x, y) // 2 1 a, _ := swap(3, 4) fmt.Println(a) // 4 }
swap
принимает два аргумента типа int
и возвращает два значения того же типа, меняя местами исходные переменные._
).openFile
возвращает два значения, одно из которых – ошибка или nil
в случае ее отсутствия.
func openFile(name string) (*File, error) { file, err := os.Open(name) if err != nil { return nil, err } return file, nil }
Интерфейсы
Используем интерфейсы правильно
package worker // worker.go type Worker interface { Work() bool } func Foo(w Worker) string { ... }
package worker // worker_test.go type secondWorker struct{ ... } func (w secondWorker) Work() bool { ... } ... if Foo(secondWorker{ ... }) == "value" { ... }
// Плохая практика package employer type Worker interface { Worker() bool } type defaultWorker struct{ ... } func (t defaultWorker) Work() bool { ... } func NewWorker() Worker { return defaultWorker{ ... } }
Worker
имитировать реализацию employer
:
// Хорошая практика package employer type Worker struct { ... } func (w Worker) Work() bool { ... } func NewWorker() Worker { return Worker{ ... } }
Конкурентность и параллелизм
Отслеживание горутин
go
в своей программе для запуска горутины, вы должны знать, как и когда она завершится.
func leakGoroutine() { ch := make(chan int) go func() { received := <- ch fmt.Println("Полученное значение:", received) } }
leakGoroutine
запускает горутину, которая блокирует чтение из канала ch
. В результате в него ничего не отправится, и сам он никогда не закроется. Горутина будет заблокирована навсегда, вызов функции fmt.Println
никогда не произойдет.Обнаружение утечек
leakGoroutin
с утечкой горутины:
func leakGoroutine() { go func() { time.Sleep(time.Minute) }() return nil }
func TestLeakGoroutine(t *Testing.T) { defer goleak.VerifyNone(t) if err := leak(); err != nil { t.Fatal("Fatal message") } }
found enexpected goroutines
, где указывается вершина стека с проблемной горутиной, ее состояние и идентификатор.Обработка ошибок и восстановление
Error() string
. Любой тип, реализующий этот метод, может быть использован как ошибка.
type error interface { Error() string }
Обрабатываем ошибки правильно
// плохо file, err := os.Open("filename.txt") if err == nil { // операции с файлом }
// хорошо file, err := os.Open("filename.txt") if err != nil { log.Fatal(err) // обработка ошибки } defer f.Close() // отложенный вызов функции для закрытия файла
Без паники, но с восстановлением
error
. Но что делать в тех случаях, когда её нельзя быстро восстановить? Тогда на помощь приходит встроенная функция panic
(часто её называют просто «паника»), которая завершает программу и выводит настраиваемое сообщение об ошибке.
package main import "fmt" func examplePanic() { panic("Паника - программа завершена") fmt.Println("Функция examplePanic успешно завершилась") } func main() { examplePanic() fmt.Println("Функция main успешно завершилась") }
recover
. Он позволяет остановить раскручивание стека и вернуть разработчику контроль над программой.
package main import "fmt" func Recovery() { if recoveryResult := recover(); recoveryResult != nil { fmt.Println(recoveryResult) } fmt.Println("Восстановление...") } func Panic() { defer Recovery() panic("Паника") fmt.Println("Функция Panic успешно завершилась") } func main() { Panic() fmt.Println("Функция main успешно завершилась") }
Паника Восстановление... Функция main успешно завершилась
Panic
не завершается после паники. Это происходит из-за того, что с помощью defer вызывается отложенная функция Recovery
, которая восстанавливает работу программы. Далее исполнение передается в main
, где происходит успешное завершение всего кода.Заключение
Материалы по теме
- 0 views
- 0 Comment