Эта статья разрешает эксклюзивное использование общедоступной учетной записи сообщества разработчиков Nuggets, включая, помимо прочего, редактирование, пометку оригинальности и другие права. Эта статья одобрена первоначальным автором, а перевод авторизован на платформе Nuggets.
Автор оригинала: Тит Петрик
оригинал:scene-four.org/2018/07/24/…
Я много лет пишу микросервисы Go и заканчиваю две книги по (API Foundations in Goи12 Factor Applications with Docker and Go) тема, несколько идей о том, как написать хороший код на Go
Но сначала я хочу немного пояснить читателям этой статьи. Хороший код субъективен. У вас могут быть совершенно разные представления о хорошем коде, и мы можем согласиться только с некоторыми из них. С другой стороны, мы все можем быть правы, только потому, что мы подходим к инженерным задачам с двух точек зрения и выбираем разные способы решения инженерных проблем, не означает, что разногласия — это плохой код.
Сумка
Пакеты важны, и вы можете возразить, но если вы пишете микросервисы на Go,Вы можете поместить весь свой код в один пакет. Конечно, есть и возражения:
- Поместите определенные типы в отдельные пакеты
- Поддерживать транспортно-независимый сервисный уровень
- В дополнение к сервисному уровню поддерживать уровень хранения данных (репозиторий).
Мы можем подсчитать, что минимальный номер пакета микросервиса равен 1. Если у вас есть крупная микрослужба с веб-сокетами и http-шлюзами, вам может понадобиться 5 пакетов (пакеты type, datastore, service, websocket и http).
Простые микросервисы на самом деле не заботятся об абстрагировании бизнес-логики от уровня хранения данных (репозиторий) или от транспортного уровня (веб-сокет, http). Вы можете написать простой код, преобразовать данные и ответить, и это сработает. Однако добавление дополнительных пакетов может решить некоторые проблемы. Например, если вы знакомы с принципами SOLID,S
Представляет единую ответственность. Если мы разделим на пакеты, эти пакеты могут быть одной ответственностью.
-
types
- объявить некоторые структуры, возможно, некоторые псевдонимы для структур и т. д. -
repository
- Уровень хранения данных для обработки структур хранения и чтения -
service
- Сервисный уровень, обертывающий конкретную реализацию бизнес-логики уровня хранения. -
http
,websocket
, … - транспортный уровень, используемый для вызова сервисного уровня
Конечно, в зависимости от вашего варианта использования вы можете дополнительно подразделить, например, вы можете использоватьtypes/request
иtypes/response
для лучшего разделения некоторых структур. так что вы можете иметьrequest.Message
иresponse.Message
вместоMessageRequest
иMessageResponse
. Может быть, было бы больше смысла, если бы это было разделено, как это сначала.
Однако, чтобы подчеркнуть исходную точку - если вы используете только некоторые из этих пакетов объявлений, это не имеет значения. Более крупные проекты, такие как Docker,server
Используется только под пакетомtypes
пакет, то, что ему действительно нужно. Другие пакеты, которые он использует (например, пакет ошибок), могут быть сторонними пакетами.
Также важно отметить, что внутри пакета легко совместно использовать структуры и функции, над которыми ведется работа. Если у вас есть взаимозависимые структуры, разделение их на два или более разных пакета может привести кАлмазная проблема зависимости. Решение тоже очевидное — собрать код вместе, или поместить весь код в один пакет.
Какой выбрать? В любом случае будет работать. Разделение его на несколько пакетов может сделать добавление нового кода громоздким, если мне придется следовать правилам. Потому что вам, возможно, придется изменить эти пакеты, чтобы добавить один вызов API. Переход между пакетами может привести к некоторым когнитивным издержкам, если не очень понятно, как их разместить. Во многих случаях код легче читать, если в проекте всего один или два пакета.
Вам определенно не нужно слишком много маленьких сумок.
Ошибка
Если описательные ошибки могут быть единственным инструментом, который есть у разработчиков для проверки производственных проблем. Вот почему очень важно, чтобы мы изящно обрабатывали ошибки или передавали их на какой-то уровень приложения, которое получает ошибку и регистрирует ее, если ее невозможно обработать. Вот некоторые функции, отсутствующие в стандартных типах ошибок библиотеки:
- Сообщение об ошибке без трассировки стека
- Не могу сложить ошибки
- ошибки создаются заранее
Однако, используя сторонние пакеты ошибок (мой любимыйpkg/Errors) может помочь с этими проблемами. Существуют и другие сторонние пакеты ошибок, но этотDave Cheney(бог языка Go), это своего рода стандарт в определенной степени в отношении обработки ошибок. его статьиНе просто проверяйте ошибки, обрабатывайте их изящноРекомендуется к прочтению.
неправильная трассировка стека
pkg/errors
пакет звонитerrors.New
, контекст (трассировка стека) добавляется к вновь созданной ошибке.
users_test.go:34: testing error Hello world
github.com/crusttech/crust/rbac_test.TestUsers
/go/src/github.com/crusttech/crust/rbac/users_test.go:34
testing.tRunner
/usr/local/go/src/testing/testing.go:777
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:2361
Учитывая, что полное сообщение об ошибке «Hello world», используйтеfmt.Printf
с участием%+v
параметры или аналогичные, чтобы напечатать небольшое количество контекста — отлично подходит для поиска ошибок. Вы можете точно знать, где была создана ошибка (ключевое слово). Конечно, если речь идет о стандартной библиотеке,errors
пакет и местныйerror
Тип — не предоставляет трассировки стека. Однако, используяpkg/errors
Один можно легко добавить. Например:
resp, err := u.Client.Post(fmt.Sprintf(resourcesCreate, resourceID), body)
if err != nil {
return errors.Wrap(err, "request failed")
}
В приведенном выше примереpkg/errors
Пакет добавляет контекст к ошибке, а также сообщение об ошибке ("request failed"
) и будет сгенерирована трассировка стека. позвонивerrors.Wrap
чтобы добавить трассировку стека, чтобы вы могли точно определить ошибку в этой строке.
ошибки укладки
Ваша файловая система, база данных или что-то другое может выдавать относительно плохо описанные ошибки. Например, Mysql может выдать эту ошибку приведения:
ERROR 1146 (42S02): Table 'test.no_such_table' doesn't exist
С этим не очень легко справиться. Однако вы можете использоватьerrors.Wrap(err,"database aseError")
Ставьте новые ошибки сверху. Таким образом, это может быть обработано лучше"databaseError"
Ждать.pkg/errors
пакет будет вcauser
Фактическое сообщение об ошибке хранится за интерфейсом.
type causer interface {
Cause() error
}
Таким образом, ошибки накапливаются без потери контекста. В качестве примечания, ошибка mysql являетсяошибка типа, который содержит не только информацию о строке ошибки, стоящей за ним. Это означает, что с ним потенциально можно справиться лучше:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
// Handle the permission-denied error
}
}
Этот пример исходит изthis stackoverflow thread.
неправильный предварительный экземпляр
Что именно является ошибкой? Очень просто, ошибка должна реализовать следующий интерфейс:
type error interface {
Error() string
}
существуетnet/http
В примере , этот пакет предоставляет несколько типов ошибок в качестве переменных, таких какДокументацияпоказано. Здесь невозможно добавить трассировку стека (Go не разрешает объявления исполняемого кода для глобальных var s, только объявления типов). Во-вторых, если стандартная библиотека добавляет к ошибке трассировку стека, она указывает не на то, где была возвращена ошибка, а на то, где была объявлена переменная (глобальная переменная).
Это означает, что вам все равно придется принудительно вызывать вызов в своем коде за чем-то вродеreturn errors.WithStack(ErrNotSupported)
код. Тоже не очень больно, но к сожалению нельзя просто импортироватьpkg/errors
, просто есть все существующие ошибки с трассировкой стека. если вы не использовалиerrors.New
чтобы создать экземпляр вашей ошибки, потребуются некоторые ручные вызовы.
бревно
Далее следует ведение журнала или, точнее, структурированное ведение журнала. Здесь доступно множество пакетов, похожих наsirupsen/logrus или мой любимыйAPEX/LOG. Эти пакеты также поддерживают отправку журналов на удаленные компьютеры или службы, и мы можем использовать инструменты для мониторинга этих журналов.
Когда дело доходит до стандартных пакетов ведения журналов, один вариант, который я не часто вижу, — это создать собственный регистратор и поместитьlog.LShorfile
илиlog.LUTC
Подождите, пока ему будет передан флаг, чтобы снова получить небольшой контекст, который может облегчить вашу работу, особенно при работе с серверами в разных часовых поясах.
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
LstdFlags = Ldate | Ltime // initial values for the standard logger
)
Даже если вы не создаете собственный регистратор, вы можете использоватьSetFlags
изменить регистратор по умолчанию. (playground link):
package main
import (
"log"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("Hello, playground")
}
Результат выглядит следующим образом:
2009/11/10 23:00:00 main.go:9: Hello, playground
Разве вы не хотите знать, где вы распечатали журнал? Это упростит отслеживание кода.
интерфейс
Если вы пишете интерфейс и называете параметры в интерфейсе, рассмотрите следующий фрагмент кода:
type Mover interface {
Move(context.Context, string, string) error
}
Вы знаете, что здесь представляют параметры? Просто используйте именованные параметры в интерфейсе, чтобы было понятно.
type Mover interface {
Move(context.Context, source string, destination string)
}
Я также часто вижу интерфейсы, которые используют конкретный тип в качестве возвращаемого значения. Недостаточно используемая практика заключается в объявлении интерфейса каким-либо образом на основе некоторых известных параметров структуры или интерфейса, а затем заполнении получателя результатом. Это, наверное, один из самых мощных интерфейсов в Go.
type Filler interface {
Fill(r *http.Request) error
}
func (s *YourStruct) Fill(r *http.Request) error {
// here you write your code...
}
Скорее всего, одна или несколько структур могут реализовать интерфейс. следующее:
type RequestParser interface {
Parse(r *http.Request) (*types.ServiceRequest, error)
}
Этот интерфейс возвращает конкретный тип (не интерфейс). Часто такой код загромождает интерфейсы в вашей кодовой базе, потому что каждый интерфейс имеет только одну реализацию и становится недоступным за пределами структуры пакета вашего приложения.
Советы
Если вы хотите во время компиляции убедиться, что ваша структура соответствует и полностью реализует интерфейс (или интерфейсы), вы можете сделать это:
var _ io.Reader = &YourStruct{}
var _ fmt.Stringer = &YourStruct{}
Если вам не хватает некоторых функций, требуемых этими интерфейсами, компилятор будет жаловаться. персонаж_
Указывает, что переменные отбрасываются, поэтому побочных эффектов нет, компилятор полностью оптимизирует код и игнорирует эти отброшенные строки.
пустой интерфейс
Это, вероятно, более спорный момент, чем тот, что приведен выше, но мне хочется использоватьinterface{}
Иногда очень эффективно. В случае ответов HTTP API последним шагом обычно является кодирование json, которое получает параметр интерфейса:
func (enc *Encoder) Encode(v interface{}) error
Таким образом, полностью избегается установка ответа API на конкретный тип. Я не рекомендую делать это для всех случаев, но в некоторых случаях можно полностью игнорировать конкретный тип ответа в API или, по крайней мере, уточнить смысл объявления конкретного типа. Примером, который приходит на ум, является использование анонимных структур.
body := struct {
Username string `json:"username"`
Roles []string `json:"roles,omitempty"`
}{username, roles}
Во-первых, не используйтеinterface{}
Если это так, нет способа вернуть такую структуру из функции. По-видимому, кодировщик json может принимать любой контент, поэтому (для меня) имеет смысл передать пустой интерфейс. Хотя тенденция состоит в том, чтобы объявлять конкретные типы, иногда вам может не понадобиться промежуточный слой. Пустые интерфейсы также подходят для функций, которые содержат некоторую логику и могут возвращать различные формы анонимных структур.
Исправление: возврат анонимных структур невозможен, это просто громоздко:playground
- Спасибо @Ikearens вDiscord Gophers #golang channel
Второй вариант использования — дизайн API на основе базы данных, о котором я писал ранее.связанный контент, я хотел бы отметить, что вполне возможно реализовать API, полностью управляемый базой данных. Это также означает, что добавление и изменение полейтолько в базеделается без добавления дополнительного уровня косвенности в виде ORM. Очевидно, что вам все равно нужно объявить тип, чтобы вставить данные в базу данных, но чтение данных из базы данных может опустить объявление.
// getThread fetches comments by data, order by ID
func (api *API) getThread(params *CommentListThread) (comments []interface{}, err error) {
// calculate pagination parameters
start := params.PageNumber * params.PageSize
length := params.PageSize
query := fmt.Sprintf("select * from comments where news_id=? and self_id=? and visible=1 and deleted=0 order by id %s limit %d, %d", params.Order, start, length)
err = api.db.Select(&comments, query, params.NewsID, params.SelfID)
return
}
Точно так же ваше приложение может действовать как обратный прокси-сервер или просто использовать хранилище базы данных без схемы. В этих случаях цель состоит в том, чтобы просто передать данные.
Большой нюанс (именно здесь нужно набирать структуры) заключается в том, что модификация значений интерфейса в Go — непростая задача. Вы должны привести их к различным вещам, таким как карта, срез или структура, чтобы иметь доступ к этим возвращенным данным. Если вы не можете сохранить структуру неизменной и просто передать ее из БД (или другой серверной службы) в кодировщик JSON (что потребует подтверждения конкретного типа), то, очевидно, этот шаблон не для вас. Такого пустого кода интерфейса в данном случае быть не должно. Тем не менее, пустой интерфейс — это то, что вам нужно, когда вы не хотите ничего знать о полезной нагрузке.
генерация кода
По возможности используйте генерацию кода. Если вы хотите сгенерировать моки для тестирования, если вы хотите сгенерировать код proc/GRPC или какой-либо тип генерации кода, который у вас может быть, вы можете просто сгенерировать код и зафиксировать. В случае конфликта его можно в любой момент удалить и создать заново.
Единственным возможным исключением является совершение чего-то вродеpublic_html
содержимое папки, которая содержит содержимое, которое вы будете использоватьrakyll/statikУпакованный контент. Если кто-то хочет сказать мне черезgomockСгенерированный код загрязняет историю GIT мегабайтами данных при каждом коммите? Не буду.
заключительные замечания
Еще одна хорошая книга о лучших и худших практиках в Go, о которой стоит упомянуть, должна бытьIdiomatic Go. Если вы не знакомы, прочтите — она хорошо сочетается с этой статьей.
Я хотел бы процитировать здесьJeff Atwood post - The Best Code is No Code At AllПредложение статьи, которое является незабываемым заключительным словом:
Если вам действительно нравится писать код, вам действительно хотелось бы писать как можно меньше кода.
Однако обязательно напишите эти модульные тесты.конец.