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-кода на все случаи жизни

  • 3 views
  • 0 Comment

Leave a Reply

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

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

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