Evitare linee eccessivamente lunghe
Evitare l'uso di righe di codice che richiedono ai lettori di fare scorrimento orizzontale o di ruotare eccessivamente il documento.
Si consiglia di limitare la lunghezza della riga a 99 caratteri. Gli autori dovrebbero spezzare la riga prima di questo limite, ma non è una regola rigida. È permesso che il codice superi questo limite.
Coerenza
Alcuni degli standard descritti in questo documento si basano su giudizi soggettivi, scenari o contesti. Tuttavia, l'aspetto più cruciale è mantenere la coerenza.
Un codice coerente è più facile da mantenere, più razionale, richiede minori costi di apprendimento ed è più facile da migrare, aggiornare e correggere in caso di nuove convenzioni o errori.
Al contrario, includere stili di codice completamente diversi o in conflitto in una codebase porta a un aumento dei costi di manutenzione, dell'incertezza e dei bias cognitivi. Tutto ciò porta direttamente a una velocità più lenta, a revisioni del codice dolorose e a un aumento del numero di bug.
Quando si applicano questi standard a una codebase, si consiglia di apportare modifiche a livello di pacchetto (o superiore). Applicare stili multipli a livello di sotto-pacchetto viola le preoccupazioni sopra elencate.
Raggruppare dichiarazioni simili
Il linguaggio Go supporta il raggruppamento di dichiarazioni simili.
Non consigliato:
import "a"
import "b"
Consigliato:
import (
"a"
"b"
)
Ciò si applica anche alle dichiarazioni di costanti, variabili e tipi:
Non consigliato:
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
Consigliato:
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
Raggruppare solo dichiarazioni correlate insieme e evitare di raggruppare dichiarazioni non correlate.
Non consigliato:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
)
Consigliato:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = "MY_ENV"
Non ci sono restrizioni su dove utilizzare il raggruppamento. Ad esempio, è possibile utilizzarli all'interno di una funzione:
Non consigliato:
func f() string {
red := color.New(0xff0000)
green := color.New(0x00ff00)
blue := color.New(0x0000ff)
...
}
Consigliato:
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
Eccezione: se le dichiarazioni delle variabili sono adiacenti ad altre variabili, specialmente all'interno delle dichiarazioni locali della funzione, dovrebbero essere raggruppate insieme. Effettuare questa operazione anche per variabili non correlate dichiarate insieme.
Non consigliato:
func (c *client) request() {
caller := c.name
format := "json"
timeout := 5*time.Second
var err error
// ...
}
Consigliato:
func (c *client) request() {
var (
caller = c.name
format = "json"
timeout = 5*time.Second
err error
)
// ...
}
Raggruppamento delle importazioni
Le importazioni dovrebbero essere raggruppate in due categorie:
- Libreria standard
- Altre librerie
Per impostazione predefinita, questo è il raggruppamento applicato da goimports.
Non consigliato:
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
Consigliato:
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
Nome del pacchetto
Quando si assegna un nome a un pacchetto, si prega di seguire queste regole:
- Tutte in minuscolo, nessuna lettera maiuscola o trattini bassi.
- Nella maggior parte dei casi, non c'è bisogno di rinominare durante l'importazione.
- Breve e conciso. Ricordatevi che il nome è completamente qualificato ovunque venga utilizzato.
- Evitare i plurali. Ad esempio, utilizzare
net/url
invece dinet/urls
. - Evitare di utilizzare "common," "util," "shared" o "lib." Questi non forniscono informazioni sufficienti.
Nomenclatura delle funzioni
Noi aderiamo alla convenzione della comunità Go nell'utilizzo di MixedCaps per i nomi delle funzioni. Viene fatta un'eccezione per raggruppare i casi di test correlati, dove il nome della funzione può contenere trattini bassi, ad esempio: TestMyFunction_WhatIsBeingTested
.
Alias di importazione
Se il nome del pacchetto non corrisponde all'ultimo elemento del percorso di importazione, deve essere utilizzato un alias di importazione.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
In tutti gli altri casi, gli alias di importazione dovrebbero essere evitati a meno che non ci sia un conflitto diretto tra gli import. Non raccomandato:
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
Raccomandato:
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
Raggruppamento e Ordine delle Funzioni
- Le funzioni dovrebbero essere approssimativamente ordinate nell'ordine in cui vengono chiamate.
- Le funzioni all'interno dello stesso file dovrebbero essere raggruppate per ricevitore.
Di conseguenza, le funzioni esportate dovrebbero apparire per prime nel file, posizionate dopo le definizioni di struct
, const
, e var
.
Una newXYZ()
/NewXYZ()
può apparire dopo le definizioni dei tipi ma prima dei metodi restanti del ricevitore.
Poiché le funzioni sono raggruppate per ricevitore, le funzioni di utilità generale dovrebbero apparire alla fine del file. Non raccomandato:
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{}
}
Raccomandato:
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 {...}
Ridurre l'Annidamento
Il codice dovrebbe ridurre l'annidamento gestendo gli errori/i casi speciali il prima possibile e restituendo o continuando il loop. Ridurre l'annidamento riduce la quantità di codice a più livelli.
Non raccomandato:
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("v non valido: %v", v)
}
}
Raccomandato:
for _, v := range data {
if v.F1 != 1 {
log.Printf("v non valido: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}
Else Non Necessario
Se una variabile viene impostata in entrambi i rami di un if, può essere sostituita con un singolo statement if.
Non raccomandato:
var a int
if b {
a = 100
} else {
a = 10
}
Raccomandato:
a := 10
if b {
a = 100
}
Dichiarazione di Variabili di Livello Superiore
Al livello superiore, utilizzare la parola chiave var
standard. Non specificare il tipo a meno che non differisca dal tipo dell'espressione.
Non raccomandato:
var _s string = F()
func F() string { return "A" }
Raccomandato:
var _s = F()
// Poiché F restituisce esplicitamente un tipo string, non è necessario specificare esplicitamente il tipo per _s
func F() string { return "A" }
Specificare il tipo se non corrisponde esattamente al tipo necessario per l'espressione.
type myError struct{}
func (myError) Error() string { return "errore" }
func F() myError { return myError{} }
var _e error = F()
// F restituisce un'istanza del tipo myError, ma abbiamo bisogno di un tipo error
Utilizzare '_' come prefisso per costanti e variabili di primo livello non esportate
Per le costanti e le variabili di primo livello non esportate, prefissarle con un trattino basso _
per indicarne esplicitamente la loro natura globale quando vengono utilizzate.
Ragionamento di base: le variabili e le costanti di primo livello hanno ambito a livello di pacchetto. L'uso di nomi generici può facilmente portare a utilizzare accidentalmente il valore errato in altri file.
Non consigliato:
// foo.go
const (
defaultPort = 8080
defaultUser = "user"
)
// bar.go
func Bar() {
defaultPort := 9090
...
fmt.Println("Porta predefinita", defaultPort)
// Non vedremo un errore di compilazione se la prima riga
// di Bar() viene eliminata.
}
Consigliato:
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
Eccezione: i valori di errore non esportati possono utilizzare il prefisso err
senza trattino basso. Consultare la denominazione degli errori.
Incorporazione nelle strutture
I tipi incorporati (come mutex) devono essere posizionati nella parte superiore dell'elenco dei campi all'interno della struttura e devono avere una riga vuota che separa i campi incorporati dai campi regolari.
Non consigliato:
type Client struct {
version int
http.Client
}
Consigliato:
type Client struct {
http.Client
version int
}
L'incorporazione dovrebbe fornire benefici tangibili, come aggiungere o migliorare la funzionalità in modo semanticamente appropriato. Dovrebbe essere utilizzata senza avere alcun impatto negativo sull'utente. (Vedi anche: Evitare di incorporare tipi nelle strutture pubbliche)
Eccezioni: Anche nei tipi non esportati, il Mutex non dovrebbe essere utilizzato come campo incorporato. Vedi anche: Il valore zero di Mutex è valido.
L'incorporazione non dovrebbe:
- Esistere esclusivamente per ragioni estetiche o di comodità.
- Rendere più difficile la costruzione o l'uso del tipo esterno.
- Avere impatto sul valore zero del tipo esterno. Se il tipo esterno ha un valore zero utile, dovrebbe comunque esserci un valore zero utile dopo l'incorporazione del tipo interno.
- Avere l'effetto collaterale di esporre funzioni o campi non correlati dal tipo interno incorporato.
- Esporre tipi non esportati.
- Avere impatto sulla forma di clonazione del tipo esterno.
- Cambiare l'API o la semantica del tipo esterno.
- Incorporare il tipo interno in una forma non standard.
- Esporre dettagli di implementazione del tipo esterno.
- Consentire agli utenti di osservare o controllare il tipo interno.
- Cambiare il comportamento generale delle funzioni interne in un modo che potrebbe sorprendere gli utenti.
In breve, incorporare consapevolmente e intenzionalmente. Un buon test per verificare è: "Verranno aggiunti direttamente tutti questi metodi/campi esportati dal tipo interno al tipo esterno?" Se la risposta è alcuni
o no
, non incorporare il tipo interno, ma utilizzare invece campi.
Non consigliato:
type A struct {
// Male: A.Lock() e A.Unlock() sono ora disponibili
// Non fornisce alcun beneficio funzionale e consente all'utente di controllare dettagli interni di A.
sync.Mutex
}
Consigliato:
type countingWriteCloser struct {
// Buono: Write() è fornito a livello esterno per uno scopo specifico,
// e delega il lavoro a Write() del tipo interno.
io.WriteCloser
count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
w.count += len(bs)
return w.WriteCloser.Write(bs)
}
Dichiarazioni di variabili locali
Se una variabile è esplicitamente impostata su un valore, dovrebbe essere utilizzata la forma di dichiarazione di variabile breve (:=
).
Non consigliato:
var s = "foo"
Consigliato:
s := "foo"
Tuttavia, in alcuni casi, l'utilizzo della parola chiave var
per i valori predefiniti può essere più chiaro.
Non consigliato:
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
Consigliato:
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
nil è una slice valida
nil
è una slice valida con una lunghezza di 0, il che significa:
- Non dovresti restituire esplicitamente una slice con una lunghezza di zero. Invece, restituisci
nil
.
Non raccomandato:
if x == "" {
return []int{}
}
Raccomandato:
if x == "" {
return nil
}
- Per verificare se una slice è vuota, utilizza sempre
len(s) == 0
invece dinil
.
Non raccomandato:
func isEmpty(s []string) bool {
return s == nil
}
Raccomandato:
func isEmpty(s []string) bool {
return len(s) == 0
}
- Le slice con valore zero (dichiarate con
var
) possono essere utilizzate immediatamente senza chiamaremake()
.
Non raccomandato:
nums := []int{}
// o, nums := make([]int)
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
Raccomandato:
var nums []int
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
Ricorda, anche se una slice è nil è una slice valida, non è uguale a una slice con una lunghezza di 0 (una è nil e l'altra no), e potrebbero essere trattate in modo differente in diverse situazioni (ad esempio, serializzazione).
Limitare la portata delle variabili
Se possibile, cerca di limitare la portata delle variabili, a meno che non entri in conflitto con la regola di riduzione dell'annidamento.
Non raccomandato:
err := os.WriteFile(name, data, 0644)
if err != nil {
return err
}
Raccomandato:
if err := os.WriteFile(name, data, 0644); err != nil {
return err
}
Se è necessario utilizzare il risultato di una chiamata di funzione al di fuori dell'istruzione if, non cercare di limitare la portata.
Non raccomandato:
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
}
Raccomandato:
data, err := os.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
Evitare parametri non definiti
I parametri poco chiari nelle chiamate di funzione possono compromettere la leggibilità del codice. Quando il significato dei nomi dei parametri non è ovvio, aggiungi commenti in stile C (/* ... */
) ai parametri.
Non raccomandato:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
Raccomandato:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
Per l'esempio precedente, un approccio migliore potrebbe essere sostituire i tipi bool
con tipi personalizzati. In questo modo, il parametro potrebbe potenzialmente supportare più di due stati (vero/falso) in futuro.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status= iota + 1
StatusDone
// Forse avremo uno StatusInProgress in futuro.
)
func printInfo(name string, region Region, status Status)
Usare stringhe letterali grezze per evitare l'escape
Go supporta l'uso di stringhe letterali grezze, che sono indicate da " ` " per rappresentare le stringhe grezze. Nei casi in cui è richiesto l'escape, dovremmo usare questo approccio per sostituire le stringhe scritte manualmente che sono più difficili da leggere.
Possono estendersi su più righe e includere virgolette. Utilizzando queste stringhe è possibile evitare le stringhe scritte manualmente più difficili da leggere.
Non raccomandato:
wantError := "unknown name:\"test\""
Raccomandato:
wantError := `unknown error:"test"`
Inizializzare strutture
Inizializzare le strutture utilizzando i nomi dei campi
Quando si inizializza una struttura, quasi sempre è necessario specificare i nomi dei campi. Attualmente questo è imposto da go vet
.
Non raccomandato:
k := User{"John", "Doe", true}
Raccomandato:
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
Eccezione: Quando ci sono 3 o meno campi, i nomi dei campi nelle tabelle di test possono essere omessi.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
Omettere i campi con valore zero nelle strutture
Quando si inizializza una struttura con campi nominati, a meno che non venga fornito un contesto significativo, ignorare i campi con valore zero. In altre parole, lasciarli automaticamente impostati ai valori zero.
Non raccomandato:
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
}
Raccomandato:
user := User{
FirstName: "John",
LastName: "Doe",
}
Questo aiuta a ridurre le barriere nella comprensione omettendo i valori predefiniti nel contesto. Specificare solo i valori significativi.
Includere il valore zero dove i nomi dei campi forniscono un contesto significativo. Ad esempio, i casi di test in una tabella di test basata su dati possono beneficiare dalla denominazione dei campi, anche se sono valori zero.
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
Utilizzare var
per le strutture con valori zero
Se tutti i campi di una struttura vengono omessi nella dichiarazione, utilizzare var
per dichiarare la struttura.
Non raccomandato:
user := User{}
Raccomandato:
var user User
Questo distingue le strutture con valori zero da quelle con campi con valori diversi da zero, in modo simile a quanto preferiamo quando dichiariamo una slice vuota.
Inizializzare i riferimenti alle strutture
Quando si inizializzano i riferimenti alle strutture, utilizzare &T{}
invece di new(T)
per renderlo coerente con l'inizializzazione delle strutture.
Non raccomandato:
sval := T{Name: "foo"}
// incoerente
sptr := new(T)
sptr.Name = "bar"
Raccomandato:
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
Inizializzare Mappe
Per una mappa vuota, utilizzare make(..)
per inizializzarla e popolarla in modo programmatico. Questo rende l'inizializzazione della mappa diversa dalla dichiarazione in apparenza, e consente anche di aggiungere suggerimenti sulle dimensioni dopo make.
Non raccomandato:
var (
// m1 è sicuro in lettura-scrittura;
// m2 genera un errore irreversibile durante la scrittura
m1 = map[T1]T2{}
m2 map[T1]T2
)
Raccomandato:
var (
// m1 è sicuro in lettura-scrittura;
// m2 genera un errore irreversibile durante la scrittura
m1 = make(map[T1]T2)
m2 map[T1]T2
)
| La dichiarazione e l'inizializzazione sembrano molto simili. | La dichiarazione e l'inizializzazione sembrano molto diverse. |
Quando possibile, fornire la dimensione della mappa durante l'inizializzazione, vedere Specificare la Dimensione delle Mappe per i dettagli.
Inoltre, se la mappa contiene un elenco fisso di elementi, utilizzare letterali di mappa per inizializzare la mappa.
Non raccomandato:
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
Raccomandato:
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
}
La guida di base è quella di utilizzare i letterali di mappa per aggiungere un insieme fisso di elementi durante l'inizializzazione. In caso contrario, utilizzare make
(e, se possibile, specificare la capacità della mappa).
Formato della Stringa per le Funzioni di Stampa-stile Printf
Se si dichiara una stringa di formato per una funzione in stile Printf
al di fuori di una funzione, impostarla come costante const
.
Questo aiuta go vet
a eseguire un'analisi statica sulla stringa di formato.
Non raccomandato:
msg := "valori inaspettati %v, %v\n"
fmt.Printf(msg, 1, 2)
Raccomandato:
const msg = "valori inaspettati %v, %v\n"
fmt.Printf(msg, 1, 2)
Nomenclatura delle funzioni nello stile Printf
Quando si dichiarano le funzioni nello stile Printf
, assicurarsi che go vet
possa rilevare e controllare la stringa di formato.
Ciò significa che è necessario utilizzare i nomi delle funzioni nello stile Printf
predefiniti il più possibile. go vet
controllerà questi nomi per impostazione predefinita. Per ulteriori informazioni, consultare Printf Family.
Se non è possibile utilizzare i nomi predefiniti, terminare il nome selezionato con f
: Wrapf
invece di Wrap
. go vet
può richiedere di controllare nomi specifici nello stile Printf, ma il nome deve terminare con f
.
go vet -printfuncs=wrapf,statusf