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
zamiastnet/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
zamiastnil
.
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ływaniamake()
.
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