Лучшие практики Golang (20 лучших)

Введение

В этом уроке мы рассмотрим 20 лучших практик кодинга на Golang. Это поможет вам писать эффективный код на языке Go.

@Golang_google – наш телеграм канал для Golang разработчиков.

20: Используйте правильные отступы

Правильные отступы делают ваш код читаемым. Последовательно используйте табуляции или пробелы (предпочтительно табуляции) и следуйте стандартному соглашению Go для отступов.

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        fmt.Println("Hello, World!")
    }
}

Выполните команду gofmt для автоматического форматирования (отступов) кода в соответствии со стандартом Go.

$ gofmt -w your_file.go

19: Правильный импорт пакетов

Импортируйте только те пакеты, которые вам нужны, и оформляйте раздел импорта так, чтобы сгруппировать пакеты стандартной библиотеки, пакеты сторонних разработчиков и собственные пакеты.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

18: Используйте понятные имена переменных и функций

  1. Значимые имена: Используйте имена, передающие назначение переменной.
  2. CamelCase: Начинать со строчной буквы и писать первую букву каждого последующего слова в имени с заглавной.
  3. Короткие имена: Короткие, лаконичные имена допустимы для короткоживущих переменных с небольшой областью применения.
  4. Отсутствие аббревиатур: Избегайте загадочных сокращений и акронимов, предпочитая описательные имена.
  5. Последовательность: Поддерживайте согласованность имен во всей кодовой базе.
package main

import "fmt"

func main() {
    // Declare variables with meaningful names
    userName := "John Doe"   // CamelCase: Start with lowercase and capitalize subsequent words.
    itemCount := 10         // Short Names: Short and concise for small-scoped variables.
    isReady := true         // No Abbreviations: Avoid cryptic abbreviations or acronyms.

    // Display variable values
    fmt.Println("User Name:", userName)
    fmt.Println("Item Count:", itemCount)
    fmt.Println("Is Ready:", isReady)
}

// Use mixedCase for package-level variables
var exportedVariable int = 42

// Function names should be descriptive
func calculateSumOfNumbers(a, b int) int {
    return a + b
}

// Consistency: Maintain naming consistency throughout your codebase.

17: Ограничение длины строки

Для улучшения читаемости по возможности не превышайте 80 символов в строках кода.

package main

import (
    "fmt"
    "math"
)

func main() {
    result := calculateHypotenuse(3, 4)
    fmt.Println("Hypotenuse:", result)
}

func calculateHypotenuse(a, b float64) float64 {
    return math.Sqrt(a*a + b*b)
}

16: Использование констант для магических значений

Избегайте магических значений в коде. Магические значения – это жестко закодированные числа или строки, разбросанные по всему коду и не имеющие контекста, что затрудняет понимание их назначения. Определите для них константы, чтобы сделать код более удобным.

package main

import "fmt"

const (
    // Define a constant for a maximum number of retries
    MaxRetries = 3

    // Define a constant for a default timeout in seconds
    DefaultTimeout = 30
)

func main() {
    retries := 0
    timeout := DefaultTimeout

    for retries < MaxRetries {
        fmt.Printf("Attempting operation (Retry %d) with timeout: %d seconds\n", retries+1, timeout)
        
        // ... Your code logic here ...

        retries++
    }
}

15: Обработка ошибок

Go поощряет разработчиков к явной обработке ошибок по следующим причинам:

  1. Безопасность: Обработка ошибок гарантирует, что неожиданные проблемы не приведут к панике или внезапному завершению программы.
  2. Ясность: Явная обработка ошибок делает код более читабельным и помогает определить, где могут возникнуть ошибки.
  3. Отладка: Обработка ошибок предоставляет ценную информацию для отладки и поиска неисправностей.

Давайте создадим простую программу, которая читает файл и правильно обрабатывает ошибки:

package main

import (
 "fmt"
 "os"
)

func main() {
 // Open a file
 file, err := os.Open("example.txt")
 if err != nil {
  // Handle the error
  fmt.Println("Error opening the file:", err)
  return
 }
 defer file.Close() // Close the file when done

 // Read from the file
 buffer := make([]byte, 1024)
 _, err = file.Read(buffer)
 if err != nil {
  // Handle the error
  fmt.Println("Error reading the file:", err)
  return
 }

 // Print the file content
 fmt.Println("File content:", string(buffer))
}

14: Избегайте глобальных переменных

Сведите к минимуму использование глобальных переменных. Глобальные переменные могут приводить к непредсказуемому поведению, затруднять отладку и препятствовать повторному использованию кода. Кроме того, они могут создавать ненужные зависимости между различными частями программы. Вместо этого передавайте данные через параметры функций и возвращаемые значения.

Напишем простую программу на языке Go, чтобы проиллюстрировать концепцию отказа от глобальных переменных:

package main

import (
 "fmt"
)

func main() {
 // Declare and initialize a variable within the main function
 message := "Hello, Go!"

 // Call a function that uses the local variable
 printMessage(message)
}

// printMessage is a function that takes a parameter
func printMessage(msg string) {
 fmt.Println(msg)
}

13: Использование структур для сложных данных

Структуры используются для объединения связанных полей данных и методов. Они позволяют группировать связанные переменные, делая код более организованным и читабельным.

Вот полный пример программы, демонстрирующий использование структур в Go:

package main

import (
    "fmt"
)

// Define a struct named Person to represent a person's information.
type Person struct {
    FirstName string // First name of the person
    LastName  string // Last name of the person
    Age       int    // Age of the person
}

func main() {
    // Create an instance of the Person struct and initialize its fields.
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // Access and print the values of the struct's fields.
    fmt.Println("First Name:", person.FirstName) // Print first name
    fmt.Println("Last Name:", person.LastName)   // Print last name
    fmt.Println("Age:", person.Age)             // Print age
}

12: Комментирование кода

Добавляйте комментарии для пояснения функциональности кода, особенно для сложных или неочевидных частей.

Однострочные комментарии

Однострочные комментарии начинаются с //. Они используются для пояснения конкретных строк кода.

package main

import "fmt"

func main() {
    // This is a single-line comment
    fmt.Println("Hello, World!") // Print a greeting
}

Многострочные комментарии

Многострочные комментарии заключаются в /* */. Они используются для длинных пояснений или комментариев, занимающих несколько строк.

package main

import "fmt"

func main() {
    /*
        This is a multi-line comment.
        It can span several lines.
    */
    fmt.Println("Hello, World!") // Print a greeting
}

Комментарии к функциям

Добавляйте к функциям комментарии, поясняющие их назначение, параметры и возвращаемые значения. Используйте стиль godoc для комментариев к функциям.

package main

import "fmt"

// greetUser greets a user by name.
// Parameters:
//   name (string): The name of the user to greet.
// Returns:
//   string: The greeting message.
func greetUser(name string) string {
    return "Hello, " + name + "!"
}

func main() {
    userName := "Alice"
    greeting := greetUser(userName)
    fmt.Println(greeting)
}

Комментарии к пакету

Добавьте комментарии в верхней части файлов Go для описания назначения пакета. Используйте тот же стиль godoc.

package main

import "fmt"

// This is the main package of our Go program.
// It contains the entry point (main) function.
func main() {
    fmt.Println("Hello, World!")
}

11: Использование горутин для обеспечения параллелизма

Использование горутин для эффективного выполнения параллельных операций. Гороутины – это легкие потоки одновременного выполнения в Go. Они позволяют выполнять функции параллельно без накладных расходов, связанных с традиционными потоками. Это позволяет писать высококонкурентные и эффективные программы.

Продемонстрируем это на простом примере:

package main

import (
 "fmt"
 "time"
)

// Function that runs concurrently
func printNumbers() {
 for i := 1; i <= 5; i++ {
  fmt.Printf("%d ", i)
  time.Sleep(100 * time.Millisecond)
 }
}

// Function that runs in the main goroutine
func main() {
 // Start the goroutine
 go printNumbers()

 // Continue executing main
 for i := 0; i < 2; i++ {
  fmt.Println("Hello")
  time.Sleep(200 * time.Millisecond)
 }
 // Ensure the goroutine completes before exiting
 time.Sleep(1 * time.Second)
}

10: Устранение паники с помощью Recover

Используйте функцию recover для изящной обработки паники и предотвращения аварийного завершения программы. В Go паники – это неожиданные ошибки времени выполнения, которые могут привести к аварийному завершению программы. Однако для изящной обработки паники в Go предусмотрен механизм, называемый recover.

Продемонстрируем это на простом примере:

package main

import "fmt"

// Function that might panic
func riskyOperation() {
 defer func() {
  if r := recover(); r != nil {
   // Recover from the panic and handle it gracefully
   fmt.Println("Recovered from panic:", r)
  }
 }()

 // Simulate a panic condition
 panic("Oops! Something went wrong.")
}

func main() {
 fmt.Println("Start of the program.")

 // Call the risky operation within a function that recovers from panics
 riskyOperation()

 fmt.Println("End of the program.")
}

9: Избегайте использования функций init

Избегайте использования функций init без необходимости, так как они могут усложнить понимание и сопровождение кода.

Лучше перенести логику инициализации в обычные функции, которые вызываются явно, как правило, из главной функции. Это обеспечивает лучший контроль, повышает читаемость кода и упрощает тестирование.

Приведем простую программу на языке Go, демонстрирующую отказ от использования функций init:

package main

import (
 "fmt"
)

// InitializeConfig initializes configuration.
func InitializeConfig() {
 // Initialize configuration parameters here.
 fmt.Println("Initializing configuration...")
}

// InitializeDatabase initializes the database connection.
func InitializeDatabase() {
 // Initialize database connection here.
 fmt.Println("Initializing database...")
}

func main() {
 // Call initialization functions explicitly.
 InitializeConfig()
 InitializeDatabase()

 // Your main program logic goes here.
 fmt.Println("Main program logic...")
}

8: Использование отложенных функций для очистки ресурсов

defer позволяет отложить выполнение функции до возвращения соседней функции. Она обычно используется для таких задач, как закрытие файлов, разблокировка мьютексов или освобождение других ресурсов.

Это гарантирует, что действия по очистке будут выполнены даже при наличии ошибок.

Создадим простую программу, которая считывает данные из файла, и будем использовать отсрочку для обеспечения правильного закрытия файла, независимо от возможных ошибок:

package main

import (
 "fmt"
 "os"
)

func main() {
 // Open the file (Replace "example.txt" with your file's name)
 file, err := os.Open("example.txt")
 if err != nil {
  fmt.Println("Error opening the file:", err)
  return // Exit the program on error
 }
 defer file.Close() // Ensure the file is closed when the function exits

 // Read and print the contents of the file
 data := make([]byte, 100)
 n, err := file.Read(data)
 if err != nil {
  fmt.Println("Error reading the file:", err)
  return // Exit the program on error
 }

 fmt.Printf("Read %d bytes: %s\n", n, data[:n])
}

7: Предпочтение составных литералов функциям-конструкторам

Для создания экземпляров структур вместо функций-конструкторов используйте составные литералы.

Зачем использовать составные литералы?

Составные литералы обладают рядом преимуществ:

  1. Лаконичность
  2. Удобство чтения
  3. Гибкость

Продемонстрируем это на простом примере:

package main

import (
 "fmt"
)

// Define a struct type representing a person
type Person struct {
 FirstName string // First name of the person
 LastName  string // Last name of the person
 Age       int    // Age of the person
}

func main() {
 // Using a composite literal to create a Person instance
 person := Person{
  FirstName: "John",   // Initialize the FirstName field
  LastName:  "Doe",    // Initialize the LastName field
  Age:       30,       // Initialize the Age field
 }

 // Printing the person's information
 fmt.Println("Person Details:")
 fmt.Println("First Name:", person.FirstName) // Access and print the First Name field
 fmt.Println("Last Name:", person.LastName)   // Access and print the Last Name field
 fmt.Println("Age:", person.Age)             // Access and print the Age field
}

6: Минимизация параметров функции

В Go очень важно писать чистый и эффективный код. Одним из способов добиться этого является минимизация количества параметров функции, что может привести к созданию более удобного и читабельного кода.

Рассмотрим эту концепцию на простом примере:

package main

import "fmt"

// Option struct to hold configuration options
type Option struct {
    Port    int
    Timeout int
}

// ServerConfig is a function that accepts an Option struct
func ServerConfig(opt Option) {
    fmt.Printf("Server configuration - Port: %d, Timeout: %d seconds\n", opt.Port, opt.Timeout)
}

func main() {
    // Creating an Option struct with default values
    defaultConfig := Option{
        Port:    8080,
        Timeout: 30,
    }

    // Configuring the server with default options
    ServerConfig(defaultConfig)

    // Modifying the Port using a new Option struct
    customConfig := Option{
        Port: 9090,
    }

    // Configuring the server with custom Port value and default Timeout
    ServerConfig(customConfig)
}

В этом примере мы определяем структуру Option для хранения параметров конфигурации сервера. Вместо того чтобы передавать функции ServerConfig несколько параметров, мы используем одну Option struct, что делает код более удобным и расширяемым. Такой подход особенно полезен при наличии функций с большим количеством параметров конфигурации.

5: Используйте явные значения возврата вместо именованных значений возврата для ясности кода

Именованные возвращаемые значения широко используются в Go, но иногда они могут сделать код менее понятным, особенно в больших проектах.

Рассмотрим разницу на простом примере.

package main

import "fmt"

// namedReturn demonstrates named return values.
func namedReturn(x, y int) (result int) {
    result = x + y
    return
}

// explicitReturn demonstrates explicit return values.
func explicitReturn(x, y int) int {
    return x + y
}

func main() {
    // Named return values
    sum1 := namedReturn(3, 5)
    fmt.Println("Named Return:", sum1)

    // Explicit return values
    sum2 := explicitReturn(3, 5)
    fmt.Println("Explicit Return:", sum2)
}

В приведенном примере программы у нас есть две функции, namedReturn и explicitReturn. Вот чем они отличаются:

  • namedReturn использует именованное возвращаемое значение result. Хотя понятно, что возвращает функция, в более сложных функциях это может быть не сразу очевидно.
  • explicitReturn возвращает результат напрямую. Это более простой и явный способ.

4: Свести сложность функций к минимуму

Под сложностью функции понимается степень сложности, вложенности и разветвленности кода функции. Низкая сложность функций делает код более читаемым, удобным для сопровождения и менее подверженным ошибкам.

Рассмотрим эту концепцию на простом примере:

package main

import (
 "fmt"
)

// CalculateSum returns the sum of two numbers.
func CalculateSum(a, b int) int {
 return a + b
}

// PrintSum prints the sum of two numbers.
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() {
 // Call the PrintSum function to demonstrate minimal function complexity.
 PrintSum()
}

В приведенном выше примере программы:

  1. Мы определяем две функции, CalculateSum и PrintSum, с определенными обязанностями.
  2. CalculateSum – это простая функция, которая вычисляет сумму двух чисел.
  3. PrintSum использует CalculateSum для вычисления и печати суммы 5 и 3.
  4. Благодаря краткости функций и их нацеленности на выполнение одной задачи мы сохраняем низкую сложность функций, что улучшает читаемость и сопровождаемость кода.

3: Избегайте затенения переменных

Затенение переменных происходит, когда новая переменная с тем же именем объявляется в более узкой области видимости, что может привести к неожиданному поведению. Она скрывает внешнюю переменную с тем же именем, делая ее недоступной в этой области видимости. Для предотвращения путаницы следует избегать теневых переменных во вложенных областях видимости.

Рассмотрим пример программы:

package main

import "fmt"

func main() {
    // Declare and initialize an outer variable 'x' with the value 10.
    x := 10
    fmt.Println("Outer x:", x)

    // Enter an inner scope with a new variable 'x' shadowing the outer 'x'.
    if true {
        x := 5 // Shadowing occurs here
        fmt.Println("Inner x:", x) // Print the inner 'x', which is 5.
    }

    // The outer 'x' remains unchanged and is still accessible.
    fmt.Println("Outer x after inner scope:", x) // Print the outer 'x', which is 10.
}

2: Использование интерфейсов для абстрагирования

Абстракция

Абстракция – это фундаментальная концепция языка Go, позволяющая определять поведение без указания деталей реализации.

Интерфейсы
В Go интерфейс – это набор сигнатур методов.

Любой тип, реализующий все методы интерфейса, неявно удовлетворяет этому интерфейсу.

Это позволяет писать код, который может работать с различными типами, если они соответствуют одному и тому же интерфейсу.

Приведем один пример программы на языке Go, демонстрирующий концепцию использования интерфейсов для абстрагирования:

package main

import (
    "fmt"
    "math"
)

// Define the Shape interface
type Shape interface {
    Area() float64
}

// Rectangle struct
type Rectangle struct {
    Width  float64
    Height float64
}

// Circle struct
type Circle struct {
    Radius float64
}

// Implement the Area method for Rectangle
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Implement the Area method for Circle
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// Function to print the area of any Shape
func PrintArea(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}

func main() {
    rectangle := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 2.5}

    // Call PrintArea on rectangle and circle, both of which implement the Shape interface
    PrintArea(rectangle) // Prints the area of the rectangle
    PrintArea(circle)    // Prints the area of the circle
}

В этой единственной программе мы определяем интерфейс Shape, создаем две структуры Rectangle и Circle, каждая из которых реализует метод Area(), и используем функцию PrintArea для печати площади любой фигуры, удовлетворяющей интерфейсу Shape.

Это демонстрирует, как можно использовать интерфейсы для абстракции в Go, чтобы работать с различными типами, используя общий интерфейс.

1: Избегайте смешивания пакетов библиотек и исполняемых файлов

В Go очень важно четко разделять пакеты и исполняемые файлы, чтобы обеспечить чистоту и сопровождаемость кода.

Приведем пример структуры проекта, демонстрирующий разделение библиотеки и исполняемого файла:

myproject/
    ├── main.go
    ├── myutils/
       └── myutils.go

myutils/myutils.go:

// Package declaration - Create a separate package for utility functions
package myutils

import "fmt"

// Exported function to print a message
func PrintMessage(message string) {
 fmt.Println("Message from myutils:", message)
}

main.go:

// Main program
package main

import (
 "fmt"
 "myproject/myutils" // Import the custom package
)

func main() {
 message := "Hello, Golang!"

 // Call the exported function from the custom package
 myutils.PrintMessage(message)

 // Demonstrate the main program logic
 fmt.Println("Message from main:", message)
}
  1. В приведенном примере у нас есть два отдельных файла: myutils.go и main.go.
  2. myutils.go определяет пользовательский пакет с именем myutils. Он содержит экспортируемую функцию PrintMessage, которая печатает сообщение.
  3. main.go – это исполняемый файл, который импортирует пользовательский пакет myutils, используя его относительный путь (“myproject/myutils”).
  4. Функция main в файле main.go вызывает функцию PrintMessage из пакета myutils и печатает сообщение. Такое разделение задач позволяет сохранить код организованным и удобным для сопровождения.

Счастливого кодинга!

источник

+1
1
+1
2
+1
0
+1
0
+1
1

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *