Share This
Связаться со мной
Крути в низ
Categories
//Лучшие практики Golang: путь к чистому коду

Лучшие практики Golang: путь к чистому коду

В статье познакомимся с рекомендациями по написанию чистого кода на Go. Разберемся на примерах с особенностями языка и применим на практике основные синтаксические конструкции.

luchshie praktiki golang put k chistomu kodu b81ac47 - Лучшие практики Golang: путь к чистому коду

Работа с данными

Отличие make и new

Make и new – это встроенные механизмы для выделения памяти. Они используются в разных ситуациях и имеют свои особенности.

  • new инициализирует нулевое значение для данного типа и возвращает указатель на этот тип.
  • make используется исключительно для создания и инициализации срезов, отображений и каналов, возвращает ненулевой экземпляр указанного типа.
  • Основное отличие между ними состоит в том, что 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...> }      

Библиотека Go разработчика Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика» Библиотека Go для собеса Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса» Библиотека задач по Go Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Функции

Функции с множественным возвратом

Функции в языке Go могут возвращать несколько значений. Это называется «множественным возвратом». Данная особенность языка позволяет возвращать не только результат, но и дополнительные значения, такие как ошибки или другие необходимые данные.

Пример объявления функции с множественным возвратом в Go:

         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 }      

Интерфейсы

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

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

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

         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{ ... } }      

Верное решение с точки зрения Go — вернуть конкретный тип и позволить Worker имитировать реализацию employer:

         // Хорошая практика package employer  type Worker struct { ... } func (w Worker) Work() bool { ... }  func NewWorker() Worker { 	return Worker{ 		... 	} }     

Конкурентность и параллелизм

Отслеживание горутин

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

  • Подробно горутины рассмотрены в статье «Горутины: что такое и как работают». Её прочтение поможет лучше разобраться в рассматриваемой теме.

💡 Запомните важное правило Каждый раз, когда вы используете ключевое слово go в своей программе для запуска горутины, вы должны знать, как и когда она завершится.

Если вы не знаете ответа на два приведённых вопроса, это может привести к возникновению утечек памяти.

Обратимся к примеру для иллюстрации данной ошибки:

         func leakGoroutine() { 	ch := make(chan int) 	go func() { 		received := <- ch 		fmt.Println("Полученное значение:", received) 	} }      

Здесь функция leakGoroutine запускает горутину, которая блокирует чтение из канала ch . В результате в него ничего не отправится, и сам он никогда не закроется. Горутина будет заблокирована навсегда, вызов функции fmt.Println никогда не произойдет.

Обнаружение утечек

Инженеры из Uber, которые принимают активное участие в развитии Go, создали детектор утечек горутин – пакет goleak, нацеленный на интеграцию с модульными тестами. Рассмотрим пример работы с этим инструментом на практике.

Пусть есть некая функция 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 , где указывается вершина стека с проблемной горутиной, ее состояние и идентификатор.

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

Обработка ошибок и восстановление

Ошибки в Go представлены интерфейсом error, который определяет метод Error() string. Любой тип, реализующий этот метод, может быть использован как ошибка.

         type error interface {     Error() string }      
  • Чтобы больше узнать об ошибках в Go, рекомендуется прочитать статью «Исключения в Go – это легко?». Из нее вы узнаете о том, как эффективно решать проблемные ситуации в программах.

Обрабатываем ошибки правильно

Игнорирование ошибок может привести к неопределенному поведению и усложнить отладку кода. Рассмотрим правильный способ обработки ошибок на примере работы с файлом:

         // плохо 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 успешно завершилась") }      

При возникновении паники функция завершается и происходит запуск оставшихся отложенных функций с помощью defer, а также раскручивание стека горутин. В реальных условиях разработки следует избегать подобных ситуаций, так как это ставит под угрозу бесперебойную работу программы. К счастью, авторы Go предусмотрели этот недостаток и создали механизм восстановления после паники – 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, где происходит успешное завершение всего кода.

Заключение

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

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

Материалы по теме

  • 🕸 Golang для веб-разработки: примеры использования
  • 🏃 Параллельное программирование в Go
  • 🚴 Паттерны Go-кода на все случаи жизни

  • 0 views
  • 0 Comment

Leave a Reply

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

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

Связаться со мной
Close