Избегание чрезмерно длинных строк
Избегайте использования строк кода, которые заставляют читателей горизонтально прокручивать или чрезмерно поворачивать документ.
Рекомендуем ограничивать длину строки до 99 символов. Авторы должны разбивать строку до этого предела, однако это не жесткое правило. Допускается превышение этого предела.
Согласованность
Некоторые из стандартов, изложенных в этом документе, основаны на субъективных суждениях, сценариях или контекстах. Однако наиболее важным аспектом является сохранение согласованности.
Согласованный код легче поддерживать, более разумный, требует меньше затрат на обучение и легче мигрировать, обновлять и исправлять ошибки, когда появляются новые соглашения или возникают ошибки.
Наоборот, включение нескольких полностью различных или противоречивых стилей кода в кодовую базу приводит к увеличению затрат на обслуживание, неопределенности и когнитивным предубеждениям. Все это прямо ведет к замедлению скорости, болезненным обзорам кода и увеличению числа ошибок.
Применяя эти стандарты к кодовой базе, рекомендуется вносить изменения на уровне пакета (или большего). Применение множества стилей на уровне подпакета нарушает вышеупомянутые рекомендации.
Группировка подобных объявлений
Язык Go поддерживает группировку подобных объявлений.
Не рекомендуется:
import "a"
import "b"
Рекомендуется:
import (
"a"
"b"
)
Это также относится к объявлениям констант, переменных и типов:
Не рекомендуется:
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
Рекомендуется:
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
Группируйте вместе только связанные объявления и избегайте группировки несвязанных объявлений.
Не рекомендуется:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
)
Рекомендуется:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = "MY_ENV"
Нет ограничений, где использовать группировку. Например, вы можете использовать их внутри функции:
Не рекомендуется:
func f() string {
red := color.New(0xff0000)
green := color.New(0x00ff00)
blue := color.New(0x0000ff)
...
}
Рекомендуется:
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
Исключение: если объявления переменных смежны с другими переменными, особенно в пределах локальных объявлений функций, их следует группировать вместе. Делайте это даже для несвязанных переменных, объявленных вместе.
Не рекомендуется:
func (c *client) request() {
caller := c.name
format := "json"
timeout := 5*time.Second
var err error
// ...
}
Рекомендуется:
func (c *client) request() {
var (
caller = c.name
format = "json"
timeout = 5*time.Second
err error
)
// ...
}
Группировка импортов
Импорты должны быть сгруппированы в две категории:
- Стандартная библиотека
- Другие библиотеки
По умолчанию это группировка, применяемая goimports. Не рекомендуется:
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
Рекомендуется:
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
Название пакета
При именовании пакета следуйте этим правилам:
- Все в нижнем регистре, без заглавных букв или подчеркиваний.
- В большинстве случаев не требуется переименовывать при импорте.
- Краткое и лаконичное. Помните, что имя полностью квалифицировано везде, где оно используется.
- Избегайте множественного числа. Например, используйте
net/url
вместоnet/urls
. - Избегайте использования "common," "util," "shared" или "lib." Эти названия не информативны достаточно.
Наименование функций
Мы придерживаемся соглашения сообщества Go об использовании смешанных заглавных букв для наименования функций. Исключение составляет группировка связанных тестов, где имя функции может содержать подчеркивания, например: TestMyFunction_WhatIsBeingTested
.
Псевдонимы импорта
Если имя пакета не совпадает с последним элементом пути импорта, должен использоваться псевдоним импорта.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
Во всех остальных случаях псевдонимы импорта следует избегать, если нет прямого конфликта между импортами. Не рекомендуется:
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
Рекомендуется:
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
Группировка и порядок функций
- Функции должны быть приблизительно отсортированы в порядке их вызова.
- Функции в одном файле должны быть сгруппированы по получателю.
Следовательно, экспортируемые функции должны появляться первыми в файле, размещенные после определений struct
, const
, и var
.
newXYZ()
/NewXYZ()
может появиться после определений типов, но до остальных методов получателя.
Поскольку функции сгруппированы по получателю, общие утилитарные функции должны появляться в конце файла. Не рекомендуется:
func (s *something) Cost() {
return calcCost(s.weights)
}
type something struct{ ... }
func calcCost(n []int) int {...}
func (s *something) Stop() {...}
func newSomething() *something {
return &something{}
}
Рекомендуется:
type something struct{ ... }
func newSomething() *something {
return &something{}
}
func (s *something) Cost() {
return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n []int) int {...}
Уменьшение вложенности
Код должен уменьшать вложенность путем обработки ошибок/особых случаев настолько рано, насколько это возможно, а затем либо возвращаться, либо продолжаться цикл. Уменьшение вложенности уменьшает объем кода на нескольких уровнях.
Не рекомендуется:
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
}
Рекомендуется:
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}
Ненужное else
Если переменная устанавливается в обоих ветвях if, ее можно заменить одним оператором if.
Не рекомендуется:
var a int
if b {
a = 100
} else {
a = 10
}
Рекомендуется:
a := 10
if b {
a = 100
}
Объявление переменных верхнего уровня
На верхнем уровне следует использовать стандартное ключевое слово var
. Не указывайте тип, если он не отличается от типа выражения.
Не рекомендуется:
var _s string = F()
func F() string { return "A" }
Рекомендуется:
var _s = F()
// Поскольку F явно возвращает тип string, нам не нужно явно указывать тип для _s
func F() string { return "A" }
Укажите тип, если он не совпадает с типом, необходимым для выражения.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F возвращает экземпляр типа myError, но нам нужен тип error
Используйте '_' в качестве префикса для неэкспортируемых глобальных констант и переменных верхнего уровня
Для неэкспортируемых переменных и констант верхнего уровня используйте префикс подчеркивания _
, чтобы явно указать их глобальную природу при использовании.
Основное обоснование: Переменные и константы верхнего уровня имеют область видимости пакета. Использование общих имен легко может привести к случайному использованию неправильного значения в других файлах.
Не рекомендуется:
// foo.go
const (
defaultPort = 8080
defaultUser = "user"
)
// bar.go
func Bar() {
defaultPort := 9090
...
fmt.Println("Default port", defaultPort)
// Мы не увидим сообщение об ошибке компиляции, если первая строка
// Bar() будет удалена.
}
Рекомендуется:
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
Исключение: Для неэкспортируемых значений ошибок можно использовать префикс err
без подчеркивания. См. название ошибок.
Встраивание в структуры
Встроенные типы (например, мьютексы) должны быть размещены вверху списка полей внутри структуры и должны иметь пустую строку, разделяющую встроенные поля и обычные поля.
Не рекомендуется:
type Client struct {
version int
http.Client
}
Рекомендуется:
type Client struct {
http.Client
version int
}
Встраивание должно приносить материальные выгоды, такие как добавление или улучшение функциональности в семантически подходящем виде. Оно должно использоваться без негативного воздействия на пользователя. (См. также: Избегайте встраивания типов в публичные структуры)
Исключения: Даже в неэкспортируемых типах Mutex не следует использовать в качестве встроенного поля. См. также: Нулевое значение Mutex допустимо.
Встраивание не должно:
- Существовать исключительно ради эстетики или удобства.
- Усложнять создание или использование внешнего типа.
- Влиять на нулевое значение внешнего типа. Если у внешнего типа есть полезное нулевое значение, оно должно оставаться полезным после встраивания внутреннего типа.
- Иметь побочный эффект путем экспонирования несвязанных функций или полей из встроенного внутреннего типа.
- Экспонировать неэкспортированные типы.
- Влиять на форму клонирования внешнего типа.
- Изменять API или семантику типа внешнего типа.
- Встраивать внутренний тип в нестандартной форме.
- Экспонировать детали реализации внешнего типа.
- Позволять пользователям узнавать или контролировать внутренний тип.
- Изменить общее поведение внутренних функций таким образом, что пользователи могут удивиться.
Кратко говоря, встраивайте сознательно и целенаправленно. Хорошим тестом является вопрос: "Будут ли все экспортированные методы/поля из внутреннего типа непосредственно добавлены к внешнему типу?" Если ответ некоторые
или нет
, то не встраивайте внутренний тип - используйте поля вместо этого.
Не рекомендуется:
type A struct {
// Плохо: A.Lock() и A.Unlock() теперь доступны
// Не приносит функциональной выгоды и позволяет пользователю контролировать внутренние детали A.
sync.Mutex
}
Рекомендуется:
type countingWriteCloser struct {
// Хорошо: Write() предоставляется на внешнем уровне для конкретной цели,
// и делегирует выполнение работы методу Write() внутреннего типа.
io.WriteCloser
count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
w.count += len(bs)
return w.WriteCloser.Write(bs)
}
Объявление локальных переменных
Если переменная явно устанавливается в значение, следует использовать сокращенную форму объявления переменной (:=
).
Не рекомендуется:
var s = "foo"
Рекомендуется:
s := "foo"
Однако в некоторых случаях использование ключевого слова var
для значений по умолчанию может быть более ясным.
Не рекомендуется:
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
Рекомендуется:
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
nil - это допустимый срез
nil
- это допустимый срез длиной 0, что означает:
- Вы не должны явно возвращать срез длиной ноль. Вместо этого возвращайте
nil
.
Не рекомендуется:
if x == "" {
return []int{}
}
Рекомендуется:
if x == "" {
return nil
}
- Для проверки, является ли срез пустым, всегда используйте
len(s) == 0
, а неnil
.
Не рекомендуется:
func isEmpty(s []string) bool {
return s == nil
}
Рекомендуется:
func isEmpty(s []string) bool {
return len(s) == 0
}
- Срезы со значением по умолчанию (объявленные с помощью
var
) можно использовать немедленно, без вызоваmake()
.
Не рекомендуется:
nums := []int{}
// или nums := make([]int)
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
Рекомендуется:
var nums []int
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
Помните, что хотя nil-срез является допустимым срезом, он не равен срезу длиной 0 (один nil, а другой - нет) и может быть обработан по-разному в различных ситуациях (например, при сериализации).
Сужение области переменных
При возможности старайтесь сузить область переменных, если это не противоречит правилу уменьшения вложенности.
Не рекомендуется:
err := os.WriteFile(name, data, 0644)
if err != nil {
return err
}
Рекомендуется:
if err := os.WriteFile(name, data, 0644); err != nil {
return err
}
Если результат вызова функции за пределами оператора if нужен, не пытайтесь сузить область.
Не рекомендуется:
if data, err := os.ReadFile(name); err == nil {
err = cfg.Decode(data)
if err != nil {
return err
}
fmt.Println(cfg)
return nil
} else {
return err
}
Рекомендуется:
data, err := os.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
Избегайте неопределенности параметров
Неясные параметры в вызовах функций могут ухудшить читаемость. Когда значение имен параметров неясно, добавьте комментарии в стиле C (/* ... */
) к параметрам.
Не рекомендуется:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
Рекомендуется:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
В данном примере более разумным подходом было бы заменить типы bool
на пользовательские типы. Таким образом, параметр потенциально сможет поддерживать не только два состояния (истина/ложь) в будущем.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status= iota + 1
StatusDone
// Возможно, в будущем появится StatusInProgress.
)
func printInfo(name string, region Region, status Status)
Используйте сырые литералы строк для избегания экранирования
Go поддерживает использование сырых литералов строк, которые обозначаются символом " ` " для представления сырых строк. В ситуациях, когда требуется экранирование, мы должны использовать этот подход для замены более трудночитаемых строк с ручным экранированием.
Они могут занимать несколько строк и содержать кавычки. Использование этих строк позволяет избежать более трудночитаемых строк с ручным экранированием.
Не рекомендуется:
wantError := "unknown name:\"test\""
Рекомендуется:
wantError := `unknown error:"test"`
Инициализация структур
Инициализация структур с использованием имён полей
При инициализации структуры почти всегда следует указывать имена полей. В настоящее время это обязательно проверяется с помощью go vet
.
Не рекомендуется:
k := User{"John", "Doe", true}
Рекомендуется:
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
Исключение: При наличии 3 или менее полей имена полей в тестовых таблицах можно опустить.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
Опускание полей со значением нулевого значения в структурах
При инициализации структуры с именованными полями, если не предоставляется осмысленный контекст, следует игнорировать поля со значением нулевого значения. То есть, мы автоматически устанавливаем их значения в нулевые.
Не рекомендуется:
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
}
Рекомендуется:
user := User{
FirstName: "John",
LastName: "Doe",
}
Это помогает снизить барьеры при чтении, опуская значения по умолчанию в контексте. Следует указывать только осмысленные значения.
Указывайте значение нулевого значения там, где имена полей предоставляют осмысленный контекст. Например, в тестовых случаях в таблице тестов может быть полезно именование полей, даже если они имеют нулевые значения.
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
Использование var
для структур с нулевым значением
Если все поля структуры опущены при объявлении, следует использовать var
для объявления структуры.
Не рекомендуется:
user := User{}
Рекомендуется:
var user User
Это различает структуры с нулевым значением от тех, у которых есть поля с ненулевым значением, аналогично предпочтению, выраженному при объявлении пустого среза.
Инициализация ссылок на структуры
При инициализации ссылок на структуры следует использовать &T{}
вместо new(T)
, чтобы обеспечить согласованность с инициализацией структуры.
Не рекомендуется:
sval := T{Name: "foo"}
// непоследовательно
sptr := new(T)
sptr.Name = "bar"
Рекомендуется:
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
Инициализация карт
Для пустой карты следует использовать make(..)
для инициализации, и карту заполнять программным способом. Это делает инициализацию карты различной от её объявления внешне и удобно позволяет добавлять подсказки для размера после make.
Не рекомендуется:
var (
// m1 можно использовать для чтения и записи;
// m2 выбрасывает исключение при записи
m1 = map[T1]T2{}
m2 map[T1]T2
)
Рекомендуется:
var (
// m1 можно использовать для чтения и записи;
// m2 выбрасывает исключение при записи
m1 = make(map[T1]T2)
m2 map[T1]T2
)
| Объявление и инициализация выглядят очень похоже. | Объявление и инициализация выглядят очень по-разному. |
При возможности укажите размер емкости карты при инициализации, см. раздел "Указание размера карты" для получения дополнительной информации.
Кроме того, если карта содержит фиксированный список элементов, следует использовать литералы карты для её инициализации.
Не рекомендуется:
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
Рекомендуется:
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
}
Основное руководство - использовать литералы карты для добавления фиксированного набора элементов при инициализации. В противном случае используйте make
(и, при возможности, укажите размер карты).
Форматирование строки для функций в стиле Printf
Если форматированная строка функции в стиле Printf
объявляется за пределами функции, следует задать её как константу const
.
Это помогает go vet
выполнить статический анализ форматной строки.
Не рекомендуется:
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
Рекомендуется:
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
Наименование функций в стиле Printf
При объявлении функций в стиле Printf
убедитесь, что go vet
может обнаружить и проверить формат строки.
Это означает, что вам следует использовать предопределенные имена функций в стиле Printf
насколько это возможно. go vet
будет проверять их по умолчанию. Дополнительную информацию смотрите в разделе Семейство Printf.
Если предопределенные имена не могут быть использованы, то добавьте к выбранному имени суффикс f
: например, Wrapf
вместо Wrap
. go vet
может запросить проверку конкретных имен функций в стиле Printf, но имя должно оканчиваться на f
.
go vet -printfuncs=wrapf,statusf