Unikanie zbyt długich linii

Unikaj używania linii kodu, które wymagają poziomego przewijania lub nadmiernego obracania dokumentu.

Zalecamy ograniczenie długości linii do 99 znaków. Autorzy powinni przerwać linie przed tym limitem, ale nie jest to twarda reguła. Dopuszcza się przekroczenie tego limitu.

Konsekwencja

Niektóre ze standardów opisanych w tym dokumencie opierają się na subiektywnych osądach, scenariuszach lub kontekstach. Jednak najważniejszym aspektem jest utrzymywanie konsekwencji.

Konsekwentny kod jest łatwiejszy w utrzymaniu, bardziej racjonalny, wymaga mniejszego kosztu nauki i jest łatwiej przenośny, aktualizowalny oraz rozwiązywalny w razie pojawienia się nowych konwencji lub błędów.

Z kolei dołączenie wielu zupełnie różnych lub sprzecznych stylów kodowania w jednym repozytorium kodu prowadzi do zwiększonych kosztów utrzymania, niepewności oraz błędnych osądów. Wszystko to bezpośrednio przekłada się na wolniejszą pracę, bolesne przeglądy kodu oraz zwiększoną liczbę błędów.

Podczas stosowania tych standardów w repozytorium kodu, zaleca się dokonywanie zmian na poziomie pakietu (lub wyższym). Zastosowanie wielu stylów na poziomie podpakietu narusza wyżej wymienione zastrzeżenia.

Grupowanie podobnych deklaracji

Język Go obsługuje grupowanie podobnych deklaracji.

Niezalecane:

import "a"
import "b"

Zalecane:

import (
  "a"
  "b"
)

Dotyczy to również deklaracji stałych, zmiennych i typów:

Niezalecane:

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

Zalecane:

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Grupuj tylko powiązane deklaracje i unikaj grupowania niepowiązanych deklaracji.

Niezalecane:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

Zalecane:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

Nie ma ograniczeń co do miejsca stosowania grupowania. Na przykład, można ich używać wewnątrz funkcji:

Niezalecane:

func f() string {
  red := color.New(0xff0000)
  green := color.New(0x00ff00)
  blue := color.New(0x0000ff)

  ...
}

Zalecane:

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

Wyjątek: Jeśli deklaracje zmiennych są sąsiednie do innych zmiennych, szczególnie w przypadku lokalnych deklaracji funkcji, powinny być one grupowane razem. Przestrzegaj tego nawet dla niepowiązanych ze sobą zmiennych zadeklarowanych jednocześnie.

Niezalecane:

func (c *client) request() {
  caller := c.name
  format := "json"
  timeout := 5*time.Second
  var err error
  // ...
}

Zalecane:

func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )
  // ...
}

Grupowanie importów

Importy powinny być grupowane w dwie kategorie:

  • Biblioteka standardowa
  • Inne biblioteki

Domyślnie jest to grupowanie stosowane przez goimports. Niezalecane:

import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Zalecane:

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Nazwa pakietu

Podczas nazywania pakietu, proszę przestrzegać następujących zasad:

  • Wszystkie małe litery, brak liter wielkich czy podkreślników.
  • W większości przypadków nie ma potrzeby zmiany nazwy podczas importowania.
  • Krótka i zwięzła. Pamiętaj, że nazwa jest w pełni określona wszędzie, gdzie jest używana.
  • Unikaj form liczby mnogiej. Na przykład użyj net/url zamiast net/urls.
  • Unikaj używania "common," "util," "shared," lub "lib." Te nazwy nie są dostatecznie informacyjne.

Nazewnictwo funkcji

Przestrzegamy konwencji społeczności Go dotyczącej używania MixedCaps dla nazw funkcji. Wyjątek stanowi grupowanie powiązanych przypadków testowych, gdzie nazwa funkcji może zawierać podkreślenia, na przykład: TestMyFunction_WhatIsBeingTested.

Aliasy importów

Jeżeli nazwa pakietu nie odpowiada ostatniemu elementowi ścieżki importu, należy użyć aliasu importu.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

W pozostałych przypadkach aliasy importów powinny być unikane, chyba że istnieje bezpośredni konflikt między importami. Nie zalecane:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

Zalecane:

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

Grupowanie i kolejność funkcji

  • Funkcje powinny być uporządkowane w przybliżonej kolejności ich wywoływania.
  • Funkcje w tym samym pliku powinny być grupowane według odbiorcy.

Dlatego też funkcje eksportowane powinny pojawić się na początku pliku, umieszczone po definicjach struct, const i var.

Funkcja newXYZ()/NewXYZ() może pojawić się po definicjach typów, ale przed pozostałymi metodami odbiorcy.

Ponieważ funkcje są grupowane według odbiorcy, ogólne funkcje pomocnicze powinny pojawić się na końcu pliku. Nie zalecane:

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

Zalecane:

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

Redukcja zagnieżdżeń

Kod powinien ograniczać zagnieżdżenie, obsługując błędy/przypadki specjalne jak najwcześniej i zwracając wartość lub kontynuując pętlę. Redukcja zagnieżdżeń zmniejsza ilość kodu na wielu poziomach.

Nie zalecane:

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("Nieprawidłowe v: %v", v)
  }
}

Zalecane:

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Nieprawidłowe v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

Niepotrzebne else

Jeśli zmienna jest ustawiana w obu gałęziach instrukcji warunkowej, można ją zastąpić pojedynczą instrukcją warunkową.

Nie zalecane:

var a int
if b {
  a = 100
} else {
  a = 10
}

Zalecane:

a := 10
if b {
  a = 100
}

Deklaracja zmiennych na najwyższym poziomie

Na najwyższym poziomie należy używać standardowego słowa kluczowego var. Nie określaj typu, chyba że różni się on od typu wyrażenia.

Nie zalecane:

var _s string = F()

func F() string { return "A" }

Zalecane:

var _s = F()
// Ponieważ F zwraca jawnie typ string, nie trzeba jawnie określać typu dla _s

func F() string { return "A" }

Określ typ, jeśli nie jest dokładnie taki sam jak wymagany typ wyrażenia.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F zwraca instancję typu myError, ale potrzebujemy typu error
## Użyj znaku '_' jako przedrostka dla niewyeksportowanych stałych i zmiennych na najwyższym poziomie

Dla niewyeksportowanych `vars` i `consts` na najwyższym poziomie, przed nimi dodaj podkreślnik `_`, aby jasno wskazać ich globalny charakter podczas użycia.

Podstawowe uzasadnienie: Zmienne i stałe na najwyższym poziomie mają zasięg pakietowy. Użycie ogólnych nazw łatwo może prowadzić do przypadkowego użycia błędnej wartości w innych plikach.

**Nie zalecane:**
```go
// foo.go

const (
  defaultPort = 8080
  defaultUser = "użytkownik"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Domyślny port", defaultPort)

  // Nie otrzymamy błędu kompilacji, jeśli pierwsza linia
  // funkcji Bar() zostanie usunięta.
}

Zalecane:

// foo.go

const (
  _domyslnyPort = 8080
  _domyslnyUzytkownik = "użytkownik"
)

Wyjątek: Niewyeksportowane wartości błędów mogą używać przedrostka err bez podkreślenia. Patrz nazewnictwo błędów.

Osadzanie w Strukturach

Osadzane typy (takie jak mutex) powinny być umieszczone na górze listy pól wewnątrz struktury i muszą być oddzielone pustą linią od zwykłych pól.

Nie zalecane:

type Klient struct {
  wersja int
  http.Klient
}

Zalecane:

type Klient struct {
  http.Klient

  wersja int
}

Osadzanie powinno przynosić namacalne korzyści, takie jak dodawanie lub ulepszanie funkcjonalności w semantycznie odpowiedni sposób. Powinno być używane bez negatywnego wpływu na użytkownika. (Zobacz również: Unikaj osadzania typów w publicznych strukturach)

Wyjątki: Nawet w przypadku typów niewyeksportowanych, mutex nie powinien być używany jako osadzone pole. Patrz również: Zero value Mutex is valid.

Osadzanie nie powinno:

  • Istnieć wyłącznie ze względów estetycznych lub wygody.
  • Sprawiać, że zbudowanie lub użycie zewnętrznego typu staje się trudniejsze.
  • Oddziaływać na wartość zero zewnętrznego typu. Jeśli zewnętrzny typ ma użyteczną wartość zero, powinna nadal istnieć użyteczna wartość zero po osadzeniu wewnętrznego typu.
  • Powodować skutki uboczne polegające na eksponowaniu niepowiązanych funkcji lub pól z osadzonego wewnętrznego typu.
  • Eksponować typy niewyeksportowane.
  • Oddziaływać na formę klonowania zewnętrznego typu.
  • Zmieniać interfejs API lub semantykę typu zewnętrznego.
  • Osadzać wewnętrzny typ w nietypowej formie.
  • Eksponować szczegóły implementacji zewnętrznego typu.
  • Pozwalać użytkownikom obserwować lub kontrolować typ wewnętrzny.
  • Zmieniać ogólne zachowanie funkcji wewnętrznych w sposób, który może zaskakiwać użytkowników.

W skrócie, osadzaj świadomie i celowo. Dobrym testem sprawdzającym jest: "Czy wszystkie te metody/pola osadzonego typu będą bezpośrednio dodane do zewnętrznego typu?" Jeśli odpowiedź brzmi tak lub nie, nie osadzaj typu wewnętrznego - użyj zamiast tego pól.

Nie zalecane:

type A struct {
    // Źle: Możliwe jest teraz wywołanie A.Lock() i A.Unlock()
    // Nie przynosi żadnych korzyści funkcjonalnych i pozwala użytkownikowi kontrolować wewnętrzne szczegóły typu A.
    sync.Mutex
}

Zalecane:

type countingWriteCloser struct {
    // Dobrze: Write() jest udostępniane na zewnętrznym poziomie dla konkretnego celu
    // i deleguje pracę do Write() z wewnętrznego typu.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

Deklaracje Lokalnych Zmiennych

Jeśli zmienna ma być jawnie ustawiona na wartość, powinien być użyty formularz skróconej deklaracji zmiennej (:=).

Nie zalecane:

var s = "foo"

Zalecane:

s := "foo"

Jednakże w niektórych przypadkach używanie słowa kluczowego var dla wartości domyślnych może być jaśniejsze.

Nie zalecane:

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

Zalecane:

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

nil jest poprawnym wycinkiem

nil jest poprawnym wycinkiem o długości 0, co oznacza:

  • Nie powinieneś wyraźnie zwracać wycinka o długości zero. Zamiast tego zwróć nil.

Nie zalecane:

if x == "" {
  return []int{}
}

Zalecane:

if x == "" {
  return nil
}
  • Aby sprawdzić, czy wycinek jest pusty, zawsze używaj len(s) == 0 zamiast nil.

Nie zalecane:

func isEmpty(s []string) bool {
  return s == nil
}

Zalecane:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • Wartości zero wycinków (wycinki zadeklarowane za pomocą var) można użyć od razu, bez wywoływania make().

Nie zalecane:

nums := []int{}
// lub nums := make([]int)

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

Zalecane:

var nums []int

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

Pamiętaj, że chociaż wycinek nil jest poprawnym wycinkiem, to nie jest równy wycinkowi o długości 0 (jeden jest nil, a drugi nie), i mogą być one traktowane inaczej w różnych sytuacjach (np. serializacja).

Ograniczenie zakresu zmiennej

Jeśli to możliwe, spróbuj ograniczyć zakres zmiennych, chyba że koliduje to z regułą redukcji zagnieżdżeń.

Nie zalecane:

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

Zalecane:

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

Jeśli wynik wywołania funkcji poza instrukcją if potrzebuje być użyty, nie próbuj ograniczać zakresu.

Nie zalecane:

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
}

Zalecane:

data, err := os.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

Unikaj "nagich" parametrów

Niejasne parametry w wywołaniach funkcji mogą szkodzić czytelności. Gdy znaczenie nazw parametrów nie jest oczywiste, dodaj komentarze w stylu C (/* ... */) do parametrów.

Nie zalecane:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

Zalecane:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

W powyższym przykładzie lepszym podejściem może być zastąpienie typów bool niestandardowymi typami. W ten sposób parametr może potencjalnie obsługiwać więcej niż tylko dwa stany (prawda/fałsz) w przyszłości.

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // Być może będziemy mieli StatusInProgress w przyszłości.
)

func printInfo(name string, region Region, status Status)

Użyj surowych literałów łańcuchowych, aby uniknąć ucieczek

Go obsługuje użycie surowych literałów łańcuchowych, które są oznaczone przez " ` " i reprezentują surowe łańcuchy. W sytuacjach, gdzie wymagane jest uniknięcie ucieczek, powinniśmy użyć tego podejścia, aby zastąpić bardziej trudne do odczytania ręcznie uciekające łańcuchy.

Może ono obejmować wiele linii i zawierać cudzysłowy. Korzystając z tych łańcuchów, można uniknąć bardziej trudnych do odczytania ręcznie uciekających łańcuchów.

Nie zalecane:

wantError := "unknown name:\"test\""

Zalecane:

wantError := `unknown error:"test"`

Inicjalizuj struktury

Inicjalizacja struktur za pomocą nazw pól

Przy inicjalizacji struktury prawie zawsze powinny być określone nazwy pól. Obecnie jest to egzekwowane przez go vet.

Nie zalecane:

k := User{"John", "Doe", true}

Zalecane:

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Wyjątek: Gdy jest 3 lub mniej pól, nazwy pól w tabelach testowych mogą zostać pominięte.

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

Pomijanie pól o wartości zero w strukturach

Podczas inicjalizacji struktury z nazwanymi polami, chyba że dostarczony jest sensowny kontekst, zignoruj pola o wartości zero. To znaczy, pozwól nam automatycznie ustawiać je na wartości zero.

Nie zalecane:

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

Zalecane:

user := User{
  FirstName: "John",
  LastName: "Doe",
}

Pomaga to zmniejszyć bariery czytelności poprzez pominięcie wartości domyślnych w kontekście. Podawaj jedynie sensowne wartości.

Podaj wartość zero, gdy nazwy pól dostarczają sensownego kontekstu. Na przykład przypadki testowe w teście z tabelą mogą skorzystać ze nazwania pól, nawet jeśli są to wartości zerowe.

tests := []struct{
  give string
  want int
}{
  {give: "0", want: 0},
  // ...
}

Użycie var dla struktur o wartości zero

Jeśli wszystkie pola struktury są pominięte w deklaracji, użyj var, aby zadeklarować strukturę.

Nie zalecane:

user := User{}

Zalecane:

var user User

To odróżnia struktury o wartości zero od tych z polami o wartości niezerowej, podobnie jak preferujemy to przy deklarowaniu pustej tablicy.

Inicjalizacja referencji do struktury

Przy inicjalizacji referencji do struktury użyj &T{} zamiast new(T), aby było to zgodne z inicjalizacją struktury.

Nie zalecane:

sval := T{Name: "foo"}

// niekonsekwentne
sptr := new(T)
sptr.Name = "bar"

Zalecane:

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

Inicjalizacja map

Dla pustej mapy użyj make(..) do jej inicjalizacji, a mapę wypełnij programowo. Sprawia to, że inicjalizacja mapy różni się od deklaracji pod względem wyglądu, a także wygodnie umożliwia dodanie wskazówek co do rozmiaru po make.

Nie zalecane:

var (
  // m1 jest bezpieczna do odczytu i zapisu;
  // m2 wyrzuca błąd podczas zapisu
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

Zalecane:

var (
  // m1 jest bezpieczna do odczytu i zapisu;
  // m2 wyrzuca błąd podczas zapisu
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

Deklaracja i inicjalizacja wyglądają bardzo podobnie. | Deklaracja i inicjalizacja wyglądają bardzo różnie.

Jeśli to możliwe, podaj rozmiar pojemności mapy podczas inicjalizacji, patrz Specifying Map Capacity for details.

Dodatkowo, jeśli mapa zawiera stałą listę elementów, użyj literałów map do jej inicjalizacji.

Nie zalecane:

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3

Zalecane:

m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

Podstawową zasadą jest korzystanie z literałów map do dodawania stałego zestawu elementów podczas inicjalizacji. W przeciwnym razie użyj make (i jeśli to możliwe, podaj pojemność mapy).

Format łańcucha dla funkcji w stylu Printf

Jeśli deklarujesz łańcuch formatujący funkcji w stylu Printf poza funkcją, ustaw go jako stałą const.

Pomaga to go vet w przeprowadzaniu statycznej analizy łańcucha formatującego.

Nie zalecane:

msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Zalecane:

const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Nazewnictwo funkcji w stylu Printf

Przy deklarowaniu funkcji w stylu Printf, upewnij się, że narzędzie go vet jest w stanie wykryć i sprawdzić ciąg formatujący.

Oznacza to, że powinieneś używać predefiniowanych nazw funkcji w stylu Printf tak często, jak to możliwe. Narzędzie go vet będzie domyślnie sprawdzać te nazwy. Aby uzyskać więcej informacji, zobacz Printf Family.

Jeśli nie można użyć predefiniowanych nazw, zakończ wybraną nazwę literą f: Wrapf zamiast Wrap. go vet może żądać sprawdzenia określonych nazw funkcji w stylu Printf, ale nazwa musi kończyć się literą f.

go vet -printfuncs=wrapf,statusf