Éviter les lignes trop longues

Évitez d'utiliser des lignes de code qui obligent les lecteurs à faire défiler horizontalement ou à pivoter excessivement le document.

Nous recommandons de limiter la longueur de la ligne à 99 caractères. Les auteurs devraient découper la ligne avant cette limite, mais ce n'est pas une règle stricte. Il est permis que le code dépasse cette limite.

Cohérence

Certaines des normes décrites dans ce document sont basées sur des jugements subjectifs, des scénarios ou des contextes. Cependant, l'aspect le plus crucial est de maintenir la cohérence.

Un code cohérent est plus facile à maintenir, plus rationnel, exige moins de coût d'apprentissage et est plus facile à migrer, mettre à jour et corriger en cas d'émergence de nouvelles conventions ou d'erreurs.

En revanche, inclure plusieurs styles de code complètement différents ou conflictuels dans une base de code entraîne des coûts de maintenance accrus, de l'incertitude et des biais cognitifs. Tout cela se traduit directement par une vitesse plus lente, des examens de code douloureux et un nombre accru de bogues.

Lors de l'application de ces normes à une base de code, il est recommandé d'apporter des modifications au niveau du package (ou plus grand). Appliquer plusieurs styles au niveau du sous-package viole les préoccupations susmentionnées.

Regrouper les déclarations similaires

Le langage Go prend en charge le regroupement de déclarations similaires.

Non recommandé :

import "a"
import "b"

Recommandé :

import (
  "a"
  "b"
)

Cela s'applique également aux déclarations de constantes, de variables et de types :

Non recommandé :

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

Recommandé :

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Regroupez uniquement les déclarations liées ensemble et évitez de regrouper des déclarations non liées.

Non recommandé :

type Operation int

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

Recommandé :

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

Il n'y a pas de restrictions sur l'endroit où utiliser le regroupement. Par exemple, vous pouvez les utiliser dans une fonction :

Non recommandé :

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

  ...
}

Recommandé :

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

  ...
}

Exception : Si les déclarations de variables sont adjacentes à d'autres variables, en particulier dans les déclarations locales de fonctions, elles doivent être regroupées ensemble. Faites-le même pour les variables non liées déclarées ensemble.

Non recommandé :

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

Recommandé :

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

Regroupement des importations

Les importations doivent être regroupées en deux catégories :

  • Bibliothèque standard
  • Autres bibliothèques

Par défaut, c'est le regroupement appliqué par goimports. Non recommandé :

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

Recommandé :

import (
  "fmt"
  "os"

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

Nom du package

Lorsque vous nommez un package, veuillez suivre ces règles :

  • Tout en minuscules, pas de lettres majuscules ou de traits de soulignement.
  • Dans la plupart des cas, pas besoin de le renommer lors de l'importation.
  • Court et concis. Rappelez-vous que le nom est entièrement qualifié partout où il est utilisé.
  • Évitez les pluriels. Par exemple, utilisez net/url au lieu de net/urls.
  • Évitez d'utiliser "common", "util", "shared" ou "lib". Ce ne sont pas des informations assez informatives.

Nommer les fonctions

Nous respectons la convention de la communauté Go qui consiste à utiliser MixedCaps pour les noms de fonctions. Une exception est faite pour regrouper des cas de test liés, où le nom de la fonction peut contenir des traits de soulignement, par exemple : TestMaFonction_CeQuiEstTesté.

Alias d'importation

Si le nom du package ne correspond pas au dernier élément du chemin d'importation, un alias d'importation doit être utilisé.

import (
  "net/http"

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

Dans tous les autres cas, les alias d'importation doivent être évités, sauf s'il existe un conflit direct entre les importations. Non recommandé:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

Recommandé:

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

  nettrace "golang.net/x/trace"
)

Regroupement et ordre des fonctions

  • Les fonctions doivent être triées approximativement dans l'ordre dans lequel elles sont appelées.
  • Les fonctions dans le même fichier doivent être regroupées par destinataire.

Par conséquent, les fonctions exportées doivent apparaître en premier dans le fichier, placées après les définitions struct, const, et var.

Une newXYZ()/NewXYZ() peut apparaître après les définitions de type mais avant les méthodes restantes du destinataire.

À mesure que les fonctions sont regroupées par destinataire, les fonctions utilitaires générales doivent apparaître à la fin du fichier. Non recommandé:

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

Recommandé:

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

Réduire l'imbrication

Le code doit réduire l'imbrication en traitant les erreurs/cas spéciaux dès que possible et en retournant ou en continuant la boucle. Réduire l'imbrication réduit la quantité de code à plusieurs niveaux.

Non recommandé:

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 invalide : %v", v)
  }
}

Recommandé:

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

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

Condition inutile

Si une variable est définie dans les deux branches d'un if, elle peut être remplacée par une seule instruction if.

Non recommandé:

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

Recommandé:

a := 10
if b {
  a = 100
}

Déclaration de variable au niveau supérieur

Au niveau supérieur, utilisez le mot clé standard var. Ne spécifiez pas le type à moins qu'il diffère du type de l'expression.

Non recommandé:

var _s string = F()

func F() string { return "A" }

Recommandé:

var _s = F()
// Puisque F retourne explicitement un type string, nous n'avons pas besoin de spécifier explicitement le type pour _s

func F() string { return "A" }

Spécifiez le type s'il ne correspond pas exactement au type nécessaire pour l'expression.

type myError struct{}

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

func F() myError { return myError{} }

var _e error = F()
// F renvoie une instance de type myError, mais nous avons besoin du type error

Utilisez '_' comme préfixe pour les constantes et variables de niveau supérieur non exportées

Pour les vars et consts de niveau supérieur non exportées, préfixez-les avec un trait de soulignement _ pour indiquer explicitement leur nature globale lors de leur utilisation.

Raison de base : Les variables et constantes de niveau supérieur ont une portée au niveau du package. L'utilisation de noms génériques peut facilement conduire à utiliser accidentellement la mauvaise valeur dans d'autres fichiers.

Non recommandé :

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Port par défaut", defaultPort)

  // Nous ne verrons pas d'erreur de compilation si la première ligne de
  // Bar() est supprimée.
}

Recommandé :

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Exception : Les valeurs d'erreur non exportées peuvent utiliser le préfixe err sans trait de soulignement. Voir la dénomination des erreurs.

Intégration dans les structures

Les types intégrés (comme mutex) doivent être placés en haut de la liste des champs dans la structure et doivent avoir une ligne vide séparant les champs intégrés des champs réguliers.

Non recommandé :

type Client struct {
  version int
  http.Client
}

Recommandé :

type Client struct {
  http.Client

  version int
}

L'intégration doit offrir des avantages tangibles, tels que l'ajout ou l'amélioration de la fonctionnalité de manière sémantiquement appropriée. Elle doit être utilisée sans impact négatif sur l'utilisateur. (Voir aussi : Éviter l'intégration de types dans les structures publiques)

Exceptions : Même dans les types non exportés, Mutex ne doit pas être utilisé comme champ intégré. Voir aussi : Un Mutex de valeur zéro est valide.

L'intégration ne doit pas :

  • Exister uniquement pour des raisons esthétiques ou de commodité.
  • Rendre plus difficile la construction ou l'utilisation du type externe.
  • Affecter la valeur zéro du type externe. Si le type externe a une valeur zéro utile, il doit toujours y avoir une valeur zéro utile après l'intégration du type interne.
  • Avoir pour effet secondaire d'exposer des fonctions ou des champs non liés du type intégré.
  • Exposer des types non exportés.
  • Affecter la forme de clonage du type externe.
  • Modifier l'API ou la sémantique du type externe.
  • Intégrer le type interne de manière non standard.
  • Exposer des détails d'implémentation du type externe.
  • Permettre aux utilisateurs d'observer ou de contrôler le type interne.
  • Changer le comportement général des fonctions internes d'une manière qui pourrait surprendre les utilisateurs.

En résumé, intégrez de manière consciente et délibérée. Un bon test est de se demander : "Tous ces méthodes/champs exportés du type interne seront-ils directement ajoutés au type externe ?" Si la réponse est quelques-uns ou non, n'intégrez pas le type interne - utilisez plutôt des champs.

Non recommandé :

type A struct {
    // Mauvais : A.Lock() et A.Unlock() sont désormais disponibles
    // Ne fournit aucun avantage fonctionnel et permet à l'utilisateur de contrôler des détails internes d'A.
    sync.Mutex
}

Recommandé :

type countingWriteCloser struct {
    // Bien : Write() est fourni au niveau externe pour un usage spécifique, 
    // et délègue le travail à Write() du type interne.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

Déclarations de variables locales

Si une variable est explicitement initialisée à une valeur, la forme de déclaration courte de variable (:=) doit être utilisée.

Non recommandé :

var s = "foo"

Recommandé :

s := "foo"

Cependant, dans certains cas, l'utilisation du mot-clé var pour les valeurs par défaut peut être plus claire.

Non recommandé :

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

Recommandé :

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

nil is a valid slice

nil est une slice valide avec une longueur de 0, ce qui signifie :

  • Vous ne devriez pas retourner explicitement une slice avec une longueur de zéro. À la place, retournez nil.

Non recommandé:

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

Recommandé:

if x == "" {
  return nil
}
  • Pour vérifier si une slice est vide, utilisez toujours len(s) == 0 au lieu de nil.

Non recommandé:

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

Recommandé:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • Les slices ayant une valeur zéro (déclarées avec var) peuvent être utilisées immédiatement sans appeler make().

Non recommandé:

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

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

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

Recommandé:

var nums []int

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

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

Rappelez-vous, bien qu'une slice nulle soit une slice valide, elle n'est pas égale à une slice ayant une longueur de 0 (l'une est nulle et l'autre ne l'est pas), et elles peuvent être traitées différemment dans différentes situations (par exemple, la sérialisation).

Limiter la portée des variables

Si possible, essayez de limiter la portée des variables, sauf si cela entre en conflit avec la règle de réduction de l'imbrication.

Non recommandé:

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

Recommandé:

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

Si le résultat d'un appel de fonction en dehors de l'instruction if doit être utilisé, n'essayez pas de limiter la portée.

Non recommandé:

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
}

Recommandé:

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

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

fmt.Println(cfg)
return nil

Éviter les paramètres sans contexte

Des paramètres peu clairs dans les appels de fonctions peuvent nuire à la lisibilité. Lorsque la signification des noms de paramètres n'est pas évidente, ajoutez des commentaires de style C (/* ... */) aux paramètres.

Non recommandé:

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

printInfo("foo", true, true)

Recommandé:

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

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

Pour l'exemple ci-dessus, une meilleure approche pourrait être de remplacer les types bool par des types personnalisés. De cette façon, le paramètre pourrait potentiellement prendre en charge plus que seulement deux états (vrai/faux) à l'avenir.

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // Peut-être aurons-nous un StatusInProgress à l'avenir.
)

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

Utiliser des littéraux de chaîne bruts pour éviter l'échappement

Go prend en charge l'utilisation de littéraux de chaîne bruts, indiqués par " ` " pour représenter des chaînes brutes. Dans les scénarios où l'échappement est requis, nous devrions utiliser cette approche pour remplacer les chaînes échappées manuellement, plus difficiles à lire.

Cela peut s'étendre sur plusieurs lignes et inclure des guillemets. L'utilisation de ces chaînes peut éviter les chaînes échappées manuellement, plus difficiles à lire.

Non recommandé:

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

Recommandé:

wantError := `unknown error:"test"`

Initialiser des structures

Initialiser des structures en utilisant les noms de champ

Lors de l'initialisation d'une structure, les noms des champs doivent presque toujours être spécifiés. Cela est actuellement appliqué par go vet.

Non recommandé :

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

Recommandé :

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

Exception : lorsque qu'il y a 3 champs ou moins, les noms des champs dans les tables de test peuvent être omis.

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

Omettre les champs de valeur zéro dans les structures

Lors de l'initialisation d'une structure avec des champs nommés, à moins que le contexte significatif ne soit fourni, ignorez les champs avec une valeur zéro. Autrement dit, laissons-les automatiquement définis à zéro.

Non recommandé :

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

Recommandé :

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

Cela aide à réduire les barrières de lecture en omettant les valeurs par défaut dans le contexte. Spécifiez uniquement les valeurs significatives.

Inclure la valeur zéro où les noms des champs fournissent un contexte significatif. Par exemple, les cas de test dans un test basé sur une table peuvent bénéficier du nommage des champs, même s'ils sont des valeurs zéro.

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

Utiliser var pour les structures de valeur zéro

Si tous les champs d'une structure sont omis dans la déclaration, utilisez var pour déclarer la structure.

Non recommandé :

user := User{}

Recommandé :

var user User

Cela distingue les structures de valeur zéro de celles avec des champs de valeur non zéro, similaire à ce que nous préférons lors de la déclaration d'une slice vide.

Initialiser les références de structure

Lors de l'initialisation des références de structure, utilisez &T{} au lieu de new(T) pour le rendre cohérent avec l'initialisation de la structure.

Non recommandé :

sval := T{Name: "foo"}

// incohérent
sptr := new(T)
sptr.Name = "bar"

Recommandé :

sval := T{Name: "foo"}

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

Initialiser les maps

Pour une map vide, utilisez make(..) pour l'initialiser, la map sera remplie de manière programmatique. Cela rend l'initialisation de la map différente de la déclaration en apparence, et cela permet également d'ajouter des indications de taille après make.

Non recommandé :

var (
  // m1 est en lecture-écriture sécurisée ;
  // m2 provoque une panne lors de l'écriture
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

Recommandé :

var (
  // m1 est en lecture-écriture sécurisée ;
  // m2 provoque une panne lors de l'écriture
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

| La déclaration et l'initialisation ont l'air très similaires. | La déclaration et l'initialisation ont l'air très différentes. |

Dans la mesure du possible, spécifiez la taille de capacité de la map lors de l'initialisation, voir Spécification de la capacité de la map pour plus de détails.

De plus, si la map contient une liste fixe d'éléments, utilisez des littéraux de map pour l'initialiser.

Non recommandé :

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

Recommandé :

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

La ligne directrice de base est d'utiliser des littéraux de map pour ajouter un ensemble fixe d'éléments lors de l'initialisation. Sinon, utilisez make (et si possible, spécifiez la capacité de la map).

Format de chaîne pour les fonctions de style Printf

Si vous déclarez une chaîne de format pour une fonction de style Printf en dehors d'une fonction, définissez-la en tant que constante const.

Cela aide go vet à effectuer une analyse statique sur la chaîne de format.

Non recommandé :

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

Recommandé :

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

Nommer les fonctions de style Printf

Lors de la déclaration de fonctions de style Printf, assurez-vous que go vet peut détecter et vérifier la chaîne de format.

Cela signifie que vous devez utiliser autant que possible les noms de fonctions de style Printf prédéfinis. go vet les vérifiera par défaut. Pour plus d'informations, consultez Famille Printf.

Si des noms prédéfinis ne peuvent pas être utilisés, terminez le nom sélectionné par f: Wrapf au lieu de Wrap. go vet peut demander à vérifier des noms spécifiques de style Printf, mais le nom doit se terminer par f.

go vet -printfuncs=wrapf,statusf