Directives de base pour la norme de codage Golang
Utiliser defer pour libérer les ressources
Utilisez defer pour libérer les ressources telles que les fichiers et les verrous.
Non recommandé:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// Il est facile d'oublier de déverrouiller lorsqu'il existe plusieurs branches de retour
Recommandé:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// Plus lisible
Le surcoût de defer est extrêmement faible, il ne devrait donc être évité que lorsque vous pouvez prouver que le temps d'exécution de la fonction est de l'ordre de la nanoseconde. Utiliser defer pour améliorer la lisibilité en vaut la peine car le coût de leur utilisation est négligeable. Cela s'applique particulièrement aux méthodes plus importantes qui impliquent autre chose que de simples accès à la mémoire, où la consommation de ressources d'autres calculs dépasse largement celle de defer
.
La taille du canal doit être de 1 ou non mis en mémoire tampon
Les canaux devraient généralement avoir une taille de 1 ou ne pas être mis en mémoire tampon. Par défaut, les canaux ne sont pas mis en mémoire tampon avec une taille de zéro. Toute autre taille doit être strictement examinée. Nous devons considérer comment déterminer la taille, ce qui empêche le canal d'écrire sous des charges importantes et quand il est bloqué, et considérer les changements qui se produisent dans la logique du système lorsque cela se produit.
Non recommandé:
// Devrait être suffisant pour gérer toutes les situations !
c := make(chan int, 64)
Recommandé:
// Taille: 1
c := make(chan int, 1) // ou
// Canal non mis en mémoire tampon, taille = 0
c := make(chan int)
Les énumérations commencent à 1
La méthode standard pour introduire des énumérations en Go consiste à déclarer un type personnalisé et un groupe de constantes qui utilise iota. Étant donné que la valeur par défaut des variables est 0, les énumérations devraient généralement commencer par une valeur différente de zéro.
Non recommandé:
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
Recommandé:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
Dans certains cas, utiliser la valeur zéro a du sens (les énumérations commencent par zéro), par exemple, lorsque la valeur zéro est le comportement par défaut idéal.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Utilisation de atomic
Utilisez les opérations atomiques du package sync/atomic pour opérer sur les types primitifs (int32
, int64
, etc.) car il est facile d'oublier d'utiliser des opérations atomiques pour lire ou modifier des variables.
go.uber.org/atomic ajoute la sécurité des types à ces opérations en cachant le type sous-jacent. De plus, il inclut un type pratique atomic.Bool
.
Méthode non recommandée:
type foo struct {
running int32 // atomique
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// déjà en cours d'exécution…
return
}
// démarrer le Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // concurrence !
}
Méthode recommandée:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// déjà en cours d'exécution…
return
}
// démarrer le Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
Éviter les variables globales mutables
Utilisez l'approche de l'injection de dépendance pour éviter de modifier les variables globales. Cela s'applique aux pointeurs de fonction ainsi qu'à d'autres types de valeurs.
Approche non recommandée 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
Approche recommandée 1:
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
Approche non recommandée 2:
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
Approche recommandée 2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Éviter l'utilisation d'identifiants pré-déclarés
Le spécification du langage Go décrit plusieurs identifiants pré-déclarés qui ne devraient pas être utilisés dans les projets Go. Ces identifiants pré-déclarés ne devraient pas être réutilisés en tant que noms dans différents contextes, car cela masquerait les identifiants originaux dans la portée actuelle (ou toute portée imbriquée), pouvant potentiellement conduire à la confusion du code. Dans le meilleur des cas, le compilateur lèvera une erreur ; dans le pire des cas, un tel code pourrait introduire des erreurs potentielles difficiles à récupérer.
Pratique non recommandée 1:
var error string
// `error` masque implicitement l'identifiant intégré
// ou
func handleErrorMessage(error string) {
// `error` masque implicitement l'identifiant intégré
}
Pratique recommandée 1:
var errorMessage string
// `error` pointe maintenant vers l'identifiant intégré non masqué
// ou
func handleErrorMessage(msg string) {
// `error` pointe maintenant vers l'identifiant intégré non masqué
}
Pratique non recommandée 2:
type Foo struct {
// Bien que ces champs ne masquent techniquement pas, redéfinir les chaînes `error` ou `string` devient désormais ambigu.
error error
string string
}
func (f Foo) Error() error {
// `error` et `f.error` semblent visuellement similaires
return f.error
}
func (f Foo) String() string {
// `string` et `f.string` semblent visuellement similaires
return f.string
}
Pratique recommandée 2:
type Foo struct {
// `error` et `string` sont maintenant explicites.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Notez que le compilateur ne générera pas d'erreurs lors de l'utilisation des identifiants pré-déclarés, mais des outils tels que go vet
signaleront correctement ces problèmes et d'autres problèmes implicitement liés.
Évitez d'utiliser init()
Essayez d'éviter autant que possible d'utiliser init()
. Lorsque init()
est inévitable ou préféré, le code devrait essayer de :
- Assurer l'exhaustivité indépendamment de l'environnement du programme ou de l'appel.
- Éviter de compter sur l'ordre ou les effets secondaires d'autres fonctions
init()
. Bien que l'ordre deinit()
soit explicite, le code peut changer, donc la relation entre les fonctionsinit()
peut rendre le code fragile et sujet aux erreurs. - Éviter d'accéder ou de manipuler des états globaux ou environnementaux, tels que les informations sur la machine, les variables d'environnement, les répertoires de travail, les paramètres/entrées du programme, etc.
- Éviter les E/S, y compris les systèmes de fichiers, le réseau et les appels système.
Le code qui ne répond pas à ces exigences peut appartenir à l'appel de main()
(ou ailleurs dans le cycle de vie du programme) ou être écrit comme partie de main()
lui-même. En particulier, les bibliothèques destinées à être utilisées par d'autres programmes devraient prêter une attention particulière à l'exhaustivité plutôt qu'à l'accomplissement de "l'initialisation magique".
Approche non recommandée 1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
Approche recommandée 1:
var _defaultFoo = Foo{
// ...
}
// ou, pour une meilleure testabilité:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
Approche non recommandée 2:
type Config struct {
// ...
}
var _config Config
func init() {
// Mauvais : basé sur le répertoire actuel
cwd, _ := os.Getwd()
// Mauvais : E/S
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
Approche recommandée 2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// gérer l'erreur
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// gérer l'erreur
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Compte tenu des considérations ci-dessus, dans certains cas, init()
peut être plus préféré ou nécessaire, notamment :
- Ne peut pas être représenté comme une seule affectation d'une expression complexe.
- Crochets insérables, tels que
database/sql
, registres de types, etc.
Préférez spécifier la capacité de tranche lors de l'ajout
Priorisez toujours la spécification d'une valeur de capacité pour make()
lors de l'initialisation d'une tranche à ajouter.
Approche non recommandée:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Approche recommandée:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Utilisation des balises de champ dans la sérialisation de la structure
Lors de la sérialisation en JSON, YAML, ou tout autre format prenant en charge la dénomination des champs en fonction des balises, les balises pertinentes devraient être utilisées pour annotation.
Non recommandé:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Recommandé:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Possible de renommer Name en Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
En théorie, le format de sérialisation d'une structure est un contrat entre différents systèmes. Apporter des modifications à la forme de sérialisation de la structure (y compris les noms de champ) rompra ce contrat. Spécifier les noms de champ dans les balises rend le contrat explicite et aide également à prévenir les violations accidentelles du contrat à travers le refactoring ou le renommage des champs.