Советы

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

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

На Go часто обнаруживается проблема выбора различных подходов. Какие значения возвращать из функций: структуры или интерфейсы  —  решение немаловажное. Мы будто на перекрестке двух путей с их преимуществами и особенностями. Исследуем их, отыскивая скрытые жемчужины структур и интерфейсов.

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

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

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

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

Рассмотрим преимущества возвращения структур из функций Go: эксплицитность, гибкость, прямой контроль над полями и методами объекта. Изучим потенциальный недостаток  —  сильную связанность  —  и выработаем стратегии эффективного ограничения его влияния.

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

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

Получая прямой доступ к свойствам структуры, мы имеем четкое представление о том, что внутри нее, поэтому работа с данными, манипулирование ими упрощаются.

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

type Person struct { Name string Age int Email string}func NewPerson(name string, age int, email string) Person { return Person{ Name: name, Age: age, Email: email, }}func main() {

person := NewPerson(«John Doe», 30, «[email protected]»)

fmt.Println(«Name:», person.Name) fmt.Println(«Age:», person.Age) fmt.Println(«Email:», person.Email)

}

В функции NewPerson возвращается структура Person с информацией о человеке. Возвращая структуру напрямую, мы получаем явный доступ к ее полям Name, Age, Email в вызывном коде.

Благодаря такой эксплицитности необходимые данные легко извлечь и использовать.

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

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

Продемонстрируем эту гибкость, обновив пример выше:

type Person struct { Name string Age int Email string}func (p Person) Greet() { fmt.Println(«Hello, my name is», p.Name)}func main() { person := Person{ Name: «John Doe», Age: 30,

Email: «[email protected]»,

} person.Greet()

}

В структуру Person добавили метод Greet. Возвращая структуру, мы определяем конкретное поведение прямо в ее типе. В методе Greet приветствие персонализируется полем Name структуры Person.

Благодаря такой гибкости поведения инкапсулируются в самой структуре, а код чище и проще в сопровождении.

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

Сильная связанность чревата проблемами сопровождения кода и дальнейшего развития приложения. Чтобы ограничить ее влияние, важно при проектировании структур следовать рекомендациям.

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

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

Совершенно новый уровень гибкости и модульности достигается при возвращении из функций Go интерфейсов. Рассмотрим преимущества возвращения интерфейсов и сравним их со структурами.

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

С этой абстракцией обеспечивается разделение обязанностей, структура кода чище. При программировании интерфейсов важно не как, а что делается объектом.

Проиллюстрируем примером:

type Shape interface { Area() float64}type Circle struct { Radius float64}func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius}type Rectangle struct { Width float64 Height float64}func (r Rectangle) Area() float64 { return r.Width * r.Height}func GetShape() Shape { if someCondition { return Circle{Radius: 5} } else { return Rectangle{Width: 10, Height: 5} }

}

Здесь в методе Area определяется интерфейс Shape, реализуемый двумя типами структур: Circle и Rectangle.

Функцией GetShape возвращается фигура Shape: в зависимости от условия, это круг Circle или прямоугольник Rectangle.

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

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

Расширим пример выше:

func PrintArea(s Shape) { fmt.Println(«Area:», s.Area())}func main() { circle := Circle{Radius: 5} rectangle := Rectangle{Width: 10, Height: 5} PrintArea(circle) // Полиморфно обращаемся с кругом «Circle» как с фигурой «Shape» PrintArea(rectangle) // Полиморфно обращаемся с прямоугольником «Rectangle» как с фигурой «Shape»

}

Здесь в функции PrintArea в качестве параметра принимается интерфейс Shape. Передаем в нее две структуры Circle и Rectangle, полиморфно обращаясь с ними как с фигурой Shape. Так пишется переиспользуемый код для работы с общими поведениями, определенными интерфейсом: дублировать логику для каждого типа структуры не нужно.

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

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

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

Настал судьбоносный момент принятия решения. Выбрать между возвращением структур и интерфейсов  —  все равно что создать шедевр, адаптированный к задачам приложения. Добавим факторы идеального выбора.

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

Знайте приложение свое, и откроется путь, по которому вам идти.

Раскройте в себе художника с красками переиспользуемости кода и модульности. Возвращать интерфейсы  —  все равно что рисовать волшебной кистью красивые пейзажи из переиспользуемого кода.

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

Долой дублированную логику, да здравствуют элегантные многоразовые сниппеты!

Предвкушая неизведанное будущее, подумайте о влиянии потенциальных изменений и внедрении нового функционала. Кодовая база  —  ваша интерактивная среда предсказаний.

Если предвидится изменение пейзажа с появлением новых вариаций объектов или деталей реализации, ваше секретное оружие  —  интерфейсы.

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

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

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

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

Вперед, дорогой читатель, навстречу этому судьбоносному выбору структуры/интерфейса!

Читайте также:  Мультиязычность на Typescript и React

Привнесем в наше исследование магию реальных сценариев, изучив практические примеры.

Разрабатывая соцсеть, имеем функцию для извлечения пользовательских профилей. При возвращении структуры UserProfile обеспечиваются эксплицитность и четкая видимость. В вызывном коде получаем прямой доступ к полям Name, Age и Bio и отображаем их, контролируя детализированные данные профиля. Этими данными легко оперировать, форматировать их перед представлением пользователям:

type UserProfile struct { Name string Age int Bio string}func GetUserProfile(userID int) UserProfile { // Извлекаем профиль из базы данных // Выполняем необходимые преобразования или проверки return UserProfile{ Name: «John Doe», Age: 30, Bio: «Passionate about coding and exploring new technologies.», }

}

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

При возвращении интерфейса ShippingProvider взаимодействие с доставщиками упрощается: вызывной код должен «знать» только об определенных в интерфейсе методах GetShippingCost или GenerateLabel. Обеспечиваются гибкость, переиспользуемость кода, и мы легко переключаемся между доставщиками, руководствуясь бизнес-задачами:

type ShippingProvider interface { GetShippingCost(weight float64) float64 GenerateLabel(address string) string}type FedEx struct{}func (f FedEx) GetShippingCost(weight float64) float64 { // Реализуем логику для расчета стоимости доставки FedEx return 10.99}func (f FedEx) GenerateLabel(address string) string { // Реализуем логику для генерирования этикетки отгрузки FedEx return «FedEx Label»}type UPS struct{}func (u UPS) GetShippingCost(weight float64) float64 { // Реализуем логику для расчета стоимости доставки UPS return 8.99}func (u UPS) GenerateLabel(address string) string { // Реализуем логику для генерирования этикетки отгрузки UPS return «UPS Label»}func GetShippingProvider() ShippingProvider { if someCondition { return FedEx{} } else { return UPS{} }

}

В этом примере определяется интерфейс ShippingProvider с методами получения стоимости доставки и генерирования этикеток, реализуемый двумя типами структур: FedEx и UPS.

Функцией GetShippingProvider возвращается ShippingProvider, то есть в вызывном коде все доставщики взаимозаменяемы.

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

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

Выбирая между структурами и интерфейсами, помните об этих рекомендациях.

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

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

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

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

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

Вот и все. Мы исследовали возвращение структур и интерфейсов на Go. Подытожим ключевые моменты.

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

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

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

Помните: универсального подхода не существует. Выбор между возвращением структур и интерфейсов зависит от конкретных требований и целей проектирования приложения. У каждого подхода имеются свои преимущества и особенности. Каким путем идти  —  решать вам, могущественный архитектор.

При написании кода учитывайте описанные выше преимущества и особенности. Стремитесь к простоте, ясности, переиспользуемости кода. Оценивайте уровень абстракции, планируйте будущие изменения, и ваш код будет развиваться, адаптируясь к веяниям времени.

Но на этом приключения не заканчиваются. В обширной области разработки ПО еще многое предстоит изучить и открыть. Так что не ждите: следите за нами, читайте и другие наши статьи в Telegram, VK и Дзене. Раскрывайте секреты изменчивого мира технологий вместе с нами.

  • Читайте нас в Telegram, VK и Дзен
  • Перевод статьи Mou Sam Dahal: Between Returning Structs and Interfaces in Go

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

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

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

Отличие 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 return slice[:10] } func main() { res := cutSlice() fmt.Println(len(res), cap(res), &res[0]) // 10 256 }

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

// Хорошая практика — данные скопированы из временного слайса func cutSlice() []byte { slice := make([]byte, 256) fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 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 }

Библиотека 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 в случае ее отсутствия.
Читайте также:  Надо ли учить ребёнка программированию: 5 аргументов «за» и 5 «против»

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

  • Подробно горутины рассмотрены в статье «Горутины: что такое и как работают». Её прочтение поможет лучше разобраться в рассматриваемой теме.
  1. ???? Запомните важное правило Каждый раз, когда вы используете ключевое слово go в своей программе для запуска горутины, вы должны знать, как и когда она завершится.
  2. Если вы не знаете ответа на два приведённых вопроса, это может привести к возникновению утечек памяти.
  3. Обратимся к примеру для иллюстрации данной ошибки:

func leakGoroutine() { ch := make(chan int) go func() { received :=

Лучшие практики Golang

Этот документ является дополнением к двум официальным источникам, пожалуйста, обратитесь к ним как к части путешествия по лучшим практикам Golang:

  • https://github.com/golang/go/wiki/CodeReviewComments
  • https://golang.org/doc/effective_go.html

Принципы написания кода

Не пишите код, который только работает. Стремитесь писать код, который можно поддерживать — не только вы сами, но и все остальные, кто может работать над программным обеспечением в будущем.

80 процентов времени разработчик читает код, а 20 процентов — пишет и тестирует его. Поэтому, пожалуйста, сосредоточьтесь на написании читабельного кода!

ваш код не должен нуждаться в х, чтобы понять, что он делает!

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

  • KISS — расшифровывается как «Keep It Simple, Stupid». Вы можете заметить, что разработчики в начале своего пути пытаются реализовать сложный, неоднозначный дизайн.
  • DRY — «Не повторяйся». Старайтесь избегать любых дубликатов, вместо этого вы помещаете их в одну часть системы или метод.
  • YAGNI — «You Ain’t Gonna Need It». Если вы столкнулись с ситуацией, когда вы спрашиваете себя: «А как насчет добавления дополнительной (функции, кода, …и т.д.)?», то вам, вероятно, нужно переосмыслить это.
  • Чистый код важнее умного кода — Говоря о чистом коде, оставьте свое эго за дверью и забудьте о написании умного кода.
  • Избегайте преждевременной оптимизации — Проблема с преждевременной оптимизацией заключается в том, что вы никогда не можете точно знать, где будут узкие места программы, пока не узнаете об этом постфактум.
  • Единая ответственность — Каждый класс/структура, пакет/модуль или функция/метод в программе должны обеспечивать только один бит конкретной функциональности.
  • Fail fast, fail hard — принцип fail-fast означает остановку текущей операции сразу после возникновения любой непредвиденной ошибки. Соблюдение этого принципа обычно приводит к более стабильному решению.

Пакеты

Организуйте по степени ответственности

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

package models// DON'T DO IT!!!

type User struct {…}

Вместо того чтобы создавать пакет models и объявлять там все типы сущностей, тип User должен жить в пакете service-layer.

Несмотря на то, что язык Go не ограничивает место определения типов, часто хорошей практикой является группировка основных типов в верхней части файла.

Избегайте очень длинных файлов

Пакет net/http из стандартной библиотеки содержит 15734 строки в 47 файлах.

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

  • В пакете encoding/json мы находим тип Encoder, а не JSONEncoder.
  • Он упоминается как json.Encoder.

Избегайте таких имен пакетов, как base, common или util

В случае, когда утилитные функции используются во многих местах, предпочитайте несколько пакетов, каждый из которых сфокусирован на одном аспекте, а не один монолитный пакет. Например, dateutil, textutil, stringutil.

Держите основной пакет как можно меньше

Ваша функция main и пакет main должны делать как можно меньше. Это связано с тем, что пакеты main не подлежат импорту и их нельзя использовать повторно.

func main() должна разобрать флаги, открыть соединения с базами данных, регистраторами и т.д., а затем передать выполнение высокоуровневому объекту.

Concurrency

TLDR

  1. Очень трудно сделать это правильно. Постарайтесь сделать все возможное, чтобы не использовать его вообще.
  2. Это действительно трудно тестировать. Постарайтесь сделать все возможное, чтобы не использовать его вообще.

Избегайте параллелизма в вашем API

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

func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc(«/», func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, «Hello, QCon!»)
})

if err := http.ListenAndServe(«0.0.0.0:8080», mux); err != nil {
log.Fatal(err)
}
}

func serveDebug() {
if err := http.ListenAndServe(«127.0.0.1:8001», http.DefaultServeMux); err != nil {
log.Fatal(err)
}
}

func main() {
go serveDebug() // The caller is responsible for the async call
go serveApp()
select {}
}

Безопасность потоков

Как и в Java при разработке асинхронного кода в Golang, нам необходимо убедиться, что наш код является Thread-safe, и это делается с помощью sync.RWMutex.

Посмотрите на этот проект кэша in-memory и на то, как реализована потокобезопасность.

Еще одна рекомендация для достижения Thread-safe — избегать передачи указателя на goroutine.

go myFunc(&myParam) // НЕ ДЕЛАЙТЕ ЭТО!!!.

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

Дополнительное чтение

Разное

Возврат на ранней стадии вместо глубокой вложенности

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

func (b *Buffer) UnreadRune() error {
if b.lastRead = int(b.lastRead) {
b.off -= int(b.lastRead)
}

b.lastRead = opInvalid
return nil
}

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

Ошибка должна быть обработана только один раз. Запись ошибки в журнал — это обработка ошибки. Поэтому ошибку следует либо регистрировать, либо распространять, причем регистрация должна быть наименее предпочтительным способом обработки ошибки.

  1. При распространении ошибки предпочтительно обернуть ее с помощью %w с fmt.Errorf() (и не записывать в журнал).
  2. При протоколировании ошибки используйте %v для представления по умолчанию, заданного error.Error() string (интерфейс ошибок).

См. также документацию пакета fmt https://golang.org/pkg/fmt/.

  • Давайте посмотрим пример того, что мы ожидаем от REST-вызова, ведущего к проблеме БД:
  • unable to serve HTTP POST request for customer customer_test: unable to insert customer contract customer_test: unable to commit transaction.
  • Мы можем сделать это следующим образом:

func postHandler(customer string) bool {
err := insert(customer)
if err != nil {
logrus.Errorf(«unable to serve HTTP POST request for customer %s: %v», customer, err)
return false
}
return true
}

Читайте также:  Где изучать Python в 2023 году: 75 ресурсов для начинающих

func insert(contract string) error {
err := dbQuery(contract)
if err != nil {
return fmt.Errorf(«unable to insert customer contract %s: %w», contract, err)
}
return nil
}

func dbQuery(contract string) error {
// Do something then fail
return fmt.Errorf(«unable to commit transaction»)
}

Подробнее здесь

Таймауты HTTP/GRPC

Всегда устанавливайте таймауты для ваших запросов (GRPC, HTTP, DB)

//HTTP call

c := &http.Client{
Timeout: 15 * time.Second,
}

resp, err := c.Get(«»[https://deem.com/»](https://deem.com/%22)«)
//DB call
newCtx, cancel := context.WithTimeout(ctx, time.Second)
row := c.db.QueryRowContext(newCtx,` `»SELECT name FROM items WHERE id = ?»«, id)

Panic или log.Fatalf

  • Сообщение журнала идет в настроенный выход журнала, в то время как panic будет писать только в stderr.
  • Panic выведет трассировку стека, которая может вообще не иметь отношения к ошибке.
  • Отложенные функции будут выполняться при панике программы, но вызов os.Exit приводит к немедленному выходу, и отложенные функции не могут быть запущены.

В общем, используйте panic только для ошибок программирования, когда трассировка стека важна для контекста ошибки. Если сообщение не адресовано программисту, используйте log.Fatalf.

Подробнее здесь

Используйте значения Enums вместо списка констант

Don't do this!!!

const (
StatusOpen = 0
StatusClosed = 1
StatusUnknown = 2
)

Instead, use Enum

type Status uint32

const (
StatusOpen Status = iota
StatusClosed
StatusUnknown
)

Указатели! Указатели повсюду!

Передача переменной по значению создаст копию этой переменной. В то время как передача по указателю просто скопирует адрес памяти.

Следовательно, передача указателя всегда будет быстрее, не так ли?

На самом деле, это не так. В некоторых тестах передача по значению более чем в 4 раза быстрее, чем передача по указателю. Это может быть немного нелогично, верно?

Объяснение этого результата связано с тем, как управляется память в Go. Подробнее здесь

Тесты

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

Сборка Go игнорирует каталог с именем testdata, и он не будет частью конечного бинарного файла.

Он также игнорируется инструментом go, имена каталогов и файлов которого начинаются с «.» или «_». Подробнее здесь

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

Мы рекомендуем помещать все необходимые объекты/конфиги/данные в каталог testing. Имейте в виду, что тестовые данные внутри папки testing будут проигнорированы сборкой go.

Предпочитайте внутренние тесты внешним (packagename_test или просто packagename)

При написании модульных тестов для вашего пакета лучше использовать внутренние тесты (без _test). Это позволит вам тестировать каждую функцию или метод напрямую, избегая бюрократии внешнего тестирования.

Больше

Нам нравится testify

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

Мокинг

Мы используем пакет tesfify mock для легкого написания mock-объектов, которые могут быть использованы вместо реальных объектов при написании кода тестов.

Ссылки:

https://golang.org/doc/effective_go.html

Go-clean-template: шаблон чистой архитектуры для сервисов на Go

Go-clean-template — шаблон для проектов на Golang, основанный на принципах чистой архитектуры Роберта («дядюшки Боба») Мартина. Вы можете клонировать шаблон и использовать его в качестве отправной точки для создания приложения на языке Go.

Чистая архитектура

Чистая архитектура — это способ организации кода сложных систем (в том числе потенциально сложных), который, как и большинство хороших сводов архитектурных принципов, способствует строгому разделению ответственности. Изначально предполагался для использования в монолитах, но так же подходит и к микросервисам.

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

Строгое разделение ответственности помогает минимизировать сложность каждого компонента, снижает вероятность появления ошибок, а если они возникли, облегчает их устранение, так как компонент-нарушитель можно легко идентифицировать.

Разделение ответственности — ключ к соблюдению принципа «минимальных привилегий».

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

Если вы придерживаетесь принципов чистой архитектуры, то, например, высокоуровневые корпоративные бизнес-правила системы не будут иметь прямого доступа к структурам данных, передаваемых между API-сервером и мобильным приложением, и даже не будут знать о существовании таких деталей. Однако, если необходимо, их содержимое может передаваться через слои от низкоуровневых компонентов к вышестоящим чётко определёнными способами.

Как любая архитектурная основа, чистая архитектура сама по себе не убережёт от написания плохого кода, если вы не будете сознательно следовать её принципам на протяжении всего жизненного цикла приложения.Тем не менее, надеемся, что этот шаблон поможет вам правильно начать.

Краткое руководство

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

Запуск с помощью Docker (запускает Postgres и RabbitMQ):

$ make compose-up

Запуск локально с миграциями:

$ make run

Запуск интеграционных тестов с помощью Docker:

$ make compose-up-integration-test

Структура проекта

Полную информацию вы найдете в README проекта, но вот некоторые особо интересные области:

cmd/app/main.go

Конфигурация и инициализация логгера, перед передачей управления в Run в файле internal/app/app.go.

internal/app

Функция Run (которую вызывает Main) в app.go выполняет инициализацию объектов и внедрение зависимостей (подробнее об этом ниже), запускает сервер и ждёт, когда наступит время для штатного завершения.

Файл migrate.go используется для автоматической миграции базы данных. Он включается, если указан аргумент с тегом migrate. Например: 

$ go run -tags migrate ./cmd/app

internal/controller

Уровень обработчиков серверов (контроллеры MVC). Шаблон включает в себя два сервера:

  • RPC (RabbitMQ как транспорт)
  • REST HTTP (фрейморк Gin)

Маршрутизаторы запросов написаны в том же стиле:

  • обработчики группируются по области применения;
  • для каждой группы создается своя структура маршрутизатора, методы которой обрабатывают пути;
  • в структуру маршрутизатора вносится структура бизнес-логики, которая будет вызываться обработчиками.
  • internal/entity
  • Модели бизнес-логики, которые являются общими для системы и могут быть использованы в любом слое.
  • internal/usecase
  • Бизнес-логика:
  • методы группируются по областям применения (на общей основе);
  • каждая группа имеет свою собственную структуру;
  • один файл — одна структура.

config/config.yml

Конфигурация приложения. Значения из файла конфигурации можно переопределить с помощью переменных окружения. Параметры конфигурации с тегом 'env-required: true' в config.go являются обязательными, и значение должно быть указано либо в config.yml, либо в окружении, либо и там, и там.

docs/

Документация Swagger. Она автоматически генерируется библиотекой swag, поэтому нет смысла вносить в неё изменения, они всё равно не сохранятся.

integration-test/

Интеграционные тесты. Они запускаются как отдельный контейнер рядом с контейнером приложения. Rest API удобно тестировать с помощью go-hit.

Внедрение зависимости

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

Например, через конструктор New внедряем зависимость в структуру бизнес-логики. Это делает бизнес-логику независимой (и переносимой). Мы можем переопределить реализацию интерфейса, не внося изменений в пакет usecase.

package usecase

import (
// Nothing!
)

type Repository interface {
Get()
}

type UseCase struct {
repo Repository
}

func New(r Repository) *UseCase {
return &UseCase{
repo: r,
}
}

func (uc *UseCase) Do() {
uc.repo.Get()
}

Это также позволяет автоматически генерировать моки (например, с помощью mockery) и легко писать юнит-тесты.

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

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

Альтернативные проекты

  1. Конечно, наше решение не является единственным шаблоном чистой архитектуры для Go.

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

  2. go-clean-arch
  3. courses-backend
     

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

Добавить комментарий

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