Лучшие практики для эффективного кода на Golang. Часть 2.
Первая часть статьи. № 11: обработка паник с помощью «Recover»
Чтобы корректно обрабатывать паники и предотвращать аварийные завершения программ, используйте функцию recover. В Go паники — это неожиданные ошибки времени выполнения, чреватые аварийным завершением программы. Однако имеется здесь и механизм корректной обработки паник — recover.
Вот простой пример: package main import "fmt" // Функция, которая может «запаниковать» func riskyOperation() { defer func
Лучшие практики для эффективного кода на Golang. Часть 2...
№ 11: обработка паник с помощью «Recover»
Чтобы корректно обрабатывать паники и предотвращать аварийные завершения программ, используйте функцию recover
. В Go паники — это неожиданные ошибки времени выполнения, чреватые аварийным завершением программы. Однако имеется здесь и механизм корректной обработки паник — recover
.
Вот простой пример:
package main import "fmt" // Функция, которая может «запаниковать» func riskyOperation() { defer func() { if r := recover(); r != nil { // Восстанавливаемся от паники и корректно ее обрабатываем fmt.Println("Recovered from panic:", r) } }() // Имитируем ситуацию паники panic("Oops! Something went wrong.") } func main() { fmt.Println("Start of the program.") // Вызываем рискованную операцию в функции, которая восстанавливается после паник riskyOperation() fmt.Println("End of the program.") }
№ 12: функции «init»
Избегайте использования функций init
без необходимости, с ними усложняются понимание и сопровождение кода.
Переносите логику инициализации в обычные функции, вызываемые явно, как правило, из функции main
. Так повышаются контроль и удобство восприятия кода, упрощается тестирование.
Покажем, как избежать функций init
, в этой простой программе Go:
package main import ( "fmt" ) // Инициализируем конфигурацию с помощью «InitializeConfig». func InitializeConfig() { // Параметры конфигурации инициализируются здесь. fmt.Println("Initializing configuration...") } // Инициализируем подключение к базе данных с помощью «InitializeDatabase». func InitializeDatabase() { // Подключение к базе данных инициализируется здесь. fmt.Println("Initializing database...") } func main() { // Вызываем функции инициализации явно. InitializeConfig() InitializeDatabase() // Логика программы «main» находится здесь. fmt.Println("Main program logic...") }
№ 13: «defer» для очистки ресурсов
defer
позволяет отложить выполнение функции до возвращения внешней функции, применяется обычно для закрытия файлов, разблокировки мьютексов или освобождения других ресурсов.
С defer
обеспечивается выполнение действий очистки даже при наличии ошибок.
Создадим простую программу считывания данных из файла и применим defer
, обеспечив этим корректное закрытие файла независимо от любых возникающих ошибок:
package main import ( "fmt" "os" ) func main() { // Открываем файл, меняем «example.txt» на название файла file, err := os.Open("example.txt") if err != nil { fmt.Println("Error opening the file:", err) return // При ошибке выходим из программы } defer file.Close() // Когда функция завершается, файл обязательно закрывается // Считываем и выводим содержимое файла data := make([]byte, 100) n, err := file.Read(data) if err != nil { fmt.Println("Error reading the file:", err) return // При ошибке выходим из программы } fmt.Printf("Read %d bytes: %s\n", n, data[:n]) }
Примечание: пример совершенствуется добавлением обработки ошибок в функцию defer
при вызове file.Close()
— имеется возможность возвращения ошибки.
№ 14: составной литерал против функций конструктора
Создавайте экземпляры структур с помощью составных литералов, а не функций конструктора.
Преимущества составных литералов
- Лаконичность.
- Удобство восприятия.
- Гибкость.
Вот простой пример:
package main import ( "fmt" ) // Определяем тип структуры, представляющей человека type Person struct { FirstName string // Имя LastName string // Фамилия Age int // Возраст } func main() { // Создаем экземпляр «Person» с помощью составного литерала person := Person{ FirstName: "John", // Инициализируем поле «FirstName» LastName: "Doe", // Инициализируем поле «LastName» Age: 30, // Инициализируем поле «Age» } // Вывод информации о человеке fmt.Println("Person Details:") fmt.Println("First Name:", person.FirstName) // Получаем доступ к полю имени и выводим его fmt.Println("Last Name:", person.LastName) // Получаем доступ к полю фамилии и выводим ее fmt.Println("Age:", person.Age) // Получаем доступ к полю возраста и выводим его }
Примечание: есть мнение, что предпочитать составные литералы конструкторам опасно, так как это может привести к некорректным структурам. Вместо экспортирования типа Person
следует обеспечить применение корректных значений с помощью экпортированного конструктора. К тому же конструкторы предпочтительны при выполнении проверок, скажем, диапазона значений или наличия чего-либо, а также зависимых операций, например логирования при создании объекта: конструкторы нужны здесь, чтобы убедиться в допустимости состояния объекта.
№ 15: параметры функций
В Go важно писать чистый и эффективный код. Один из способов делать это — минимизировать количество параметров функций, благодаря чему код становится удобнее для восприятия и сопровождения.
Вот простой пример:
package main import "fmt" // Структура «Option» для хранения параметров конфигурации type Option struct { Port int Timeout int } // «ServerConfig» — это функция, которая принимает структуру «Option» func ServerConfig(opt Option) { fmt.Printf("Server configuration - Port: %d, Timeout: %d seconds\n", opt.Port, opt.Timeout) } func main() { // Создание структуры «Option» со значениями по умолчанию defaultConfig := Option{ Port: 8080, Timeout: 30, } // Настройка сервера с параметрами по умолчанию ServerConfig(defaultConfig) // Изменение порта «Port» с помощью новой структуры «Option» customConfig := Option{ Port: 9090, } // Настройка сервера с пользовательским значением «Port» и временем ожидания «Timeout» по умолчанию ServerConfig(customConfig) }
В этом примере определяется структура Option
для параметров конфигурации сервера. Использование одной этой структуры вместо передачи в функцию ServerConfig
многочисленных параметров добавляет коду возможностей для сопровождения и расширения.
№ 16: явные возвращаемые значения против именованных
В Go обычно используются именованные возвращаемые значения, но иногда это сказывается на ясности кода, особенно в больших кодовых базах.
Продемонстрируем разницу на простом примере:
package main import "fmt" // В «namedReturn» используются именованные возвращаемые значения. func namedReturn(x, y int) (result int) { result = x + y return } // А в «explicitReturn» — явные. func explicitReturn(x, y int) int { return x + y } func main() { // Именованные возвращаемые значения sum1 := namedReturn(3, 5) fmt.Println("Named Return:", sum1) // Явные возвращаемые значения sum2 := explicitReturn(3, 5) fmt.Println("Explicit Return:", sum2) }
Здесь две функции namedReturn
и explicitReturn
отличаются следующим:
- В
namedReturn
используется именованное возвращаемое значениеresult
. Хотя то, что возвращается функцией, здесь ясно, в функциях посложнее это не так очевидно. - В
explicitReturn
результат возвращается напрямую, так проще и понятнее.
№ 17: сложность функций
Сложностью функции называется степень запутанности, вложенности и разветвленности кода функции. Код с функциями низкой сложности удобнее для восприятия и сопровождения, менее подвержен ошибкам.
Вот простой пример:
package main import ( "fmt" ) // В «CalculateSum» возвращается сумма двух чисел. func CalculateSum(a, b int) int { return a + b } // В «PrintSum» выводится сумма двух чисел. func PrintSum() { x := 5 y := 3 sum := CalculateSum(x, y) fmt.Printf("Sum of %d and %d is %d\n", x, y, sum) } func main() { // Продемонстрируем минимальную сложность функции, вызвав функцию «PrintSum». PrintSum() }
В этом примере:
- Определяются две функции —
CalculateSum
иPrintSum
— с конкретными задачами. CalculateSum
— простая функция для вычисления суммы двух чисел.- В
PrintSum
выводится вычисляемая с помощьюCalculateSum
сумма чисел 5 и 3. - Низкая сложность функций обеспечивается их лаконичностью и акцентированностью на одной задаче, за счет этого повышаются удобство восприятия и сопровождаемость кода.
№ 18: затенение переменных
Затенение переменных происходит, когда в более узкой области объявляется новая переменная с тем же названием, и это чревато неожиданным поведением. Внешняя переменная скрывается за новой, делаясь недоступной в этой области.
Чтобы избежать путаницы, избегайте затенения переменных внутри вложенных областей.
Вот пример программы:
package main import "fmt" func main() { // Объявляем внешнюю переменную «x» и инициализируем значением «10». x := 10 fmt.Println("Outer x:", x) // Вводим внутреннюю область с новой переменной «x», затеняющей внешнюю «x». if true { x := 5 // Затенение происходит здесь fmt.Println("Inner x:", x) // Выводим внутреннюю «x» со значением «5». } // Внешняя «x» остается неизменной и по-прежнему доступна. fmt.Println("Outer x after inner scope:", x) // Выводим внешнюю «x» со значением «10». }
№ 19: интерфейсы для абстракции
Абстракция
Абстракция — это фундаментальная концепция Go, позволяющая определять поведение, не указывая детали реализации.
Интерфейсы
В Go интерфейс — это набор сигнатур методов.
Любой тип, реализующий все методы интерфейса, неявно соответствует этому интерфейсу.
Это позволяет писать код, который работает с разными типами, пока они придерживаются того же интерфейса.
Вот пример программы на Go, демонстрирующей применение интерфейсов для абстракции:
package main import ( "fmt" "math" ) // Определяем интерфейс «Shape» type Shape interface { Area() float64 } // Структура «Rectangle» type Rectangle struct { Width float64 Height float64 } // Структура «Circle» type Circle struct { Radius float64 } // Реализуем метод «Area» для прямоугольника func (r Rectangle) Area() float64 { return r.Width * r.Height } // Реализуем метод «Area» для окружности func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } // Функция для вывода площади любой фигуры func PrintArea(s Shape) { fmt.Printf("Area: %.2f\n", s.Area()) } func main() { rectangle := Rectangle{Width: 5, Height: 3} circle := Circle{Radius: 2.5} // Вызоваем «PrintArea» в прямоугольнике и окружности, ими обоими реализуется интерфейс «Shape» PrintArea(rectangle) // Выводится площадь прямоугольника PrintArea(circle) // Выводится площадь окружности }
В этой одной программе мы определяем интерфейс Shape
, создаем две структуры — Rectangle
и Circle
, каждой из которых реализуется метод Area()
, и функцией PrintArea
выводим площадь любой фигуры, соответствующей интерфейсу Shape
.
Примечание: в func PrintArea(s Shape)
надо проверить интерфейс, если он nil
, это чревато паникой.
Так применяются интерфейсы для абстракции в коде на Go, работающем с различными типами, которыми используется общий интерфейс.
№ 20: библиотечные пакеты и исполняемые файлы
Чтобы сохранять код на Go чистым и сопровождаемым, важно четко разделять пакеты и исполняемые файлы.
Вот пример структуры проекта с разделением библиотеки и исполняемого файла:
myproject/
├── main.go
├── myutils/
└── myutils.go
myutils/myutils.go
:
// Объявление пакета — создается отдельный пакет для служебных функций package myutils import "fmt" // Экспортированная функция для вывода сообщения func PrintMessage(message string) { fmt.Println("Message from myutils:", message) }
main.go
:
// Основная программа package main import ( "fmt" "myproject/myutils" // Импортируем пользовательский пакет ) func main() { message := "Hello, Golang!" // Из пользовательского пакета вызываем экспортированную функцию myutils.PrintMessage(message) // Демонстрируем логику основной программы fmt.Println("Message from main:", message) }
- В этом примере имеется два отдельных файла:
myutils.go
иmain.go
. - В
myutils.go
определяется пользовательский пакетmyutils
, содержащий экспортированную функциюPrintMessage
, которой выводится сообщение. main.go
— исполняемый файл, которым импортируется пользовательский пакетmyutils
по его относительному пути"myproject/myutils"
.- Функцией
main
вmain.go
из пакетаmyutils
вызывается функцияPrintMessage
и выводится сообщение. Такое разделение обязанностей сохраняет код организованным и сопровождаемым.