№ 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
и выводится сообщение. Такое разделение обязанностей сохраняет код организованным и сопровождаемым.
Читайте также:
- Что такое Recover в Golang?
- Go — единственный выбор для бэкенд-разработчика?
- Реализация односвязного списка в Golang
Читайте нас в Telegram, VK и Дзен
Перевод статьи Golang Da: Golang Best Practices (Top 20)