Vermeidung von übermäßig langen Zeilen

Vermeiden Sie die Verwendung von Codezeilen, die die Leser horizontal scrollen oder das Dokument übermäßig umdrehen müssen.

Wir empfehlen, die Zeilenlänge auf 99 Zeichen zu begrenzen. Autoren sollten die Zeile vor dieser Grenze umbrechen, aber es ist keine harte Regel. Es ist zulässig, dass der Code diese Grenze überschreitet.

Konsistenz

Einige der in diesem Dokument festgelegten Standards basieren auf subjektiven Beurteilungen, Szenarien oder Kontexten. Die wesentlichste Aspekte ist jedoch die Aufrechterhaltung der Konsistenz.

Konsistenter Code ist leichter zu warten, rationaler, erfordert weniger Lernaufwand und ist einfacher zu migrieren, zu aktualisieren und Fehler zu beheben, wenn neue Konventionen entstehen oder Fehler auftreten.

Im Gegensatz dazu führen das Einschließen von mehreren völlig unterschiedlichen oder widersprüchlichen Code-Stilen in einem Codebestand zu erhöhten Wartungskosten, Unsicherheit und kognitiven Verzerrungen. All dies führt direkt zu einer langsameren Geschwindigkeit, schmerzhaften Code-Reviews und einer erhöhten Anzahl von Fehlern.

Bei der Anwendung dieser Standards auf einen Codebestand wird empfohlen, Änderungen auf Paket- (oder höherem) Niveau vorzunehmen. Das Anwenden mehrerer Stile auf der Unter-Paket-Ebene verstößt gegen die oben genannten Bedenken.

Gruppierung ähnlicher Deklarationen

Die Programmiersprache Go unterstützt die Gruppierung ähnlicher Deklarationen.

Nicht empfohlen:

import "a"
import "b"

Empfohlen:

import (
  "a"
  "b"
)

Dies gilt auch für Konstanten-, Variablen- und Typdeklarationen:

Nicht empfohlen:

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

Empfohlen:

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Gruppieren Sie nur zusammenhängende Deklarationen und vermeiden Sie das Gruppieren von nicht zusammenhängenden Deklarationen.

Nicht empfohlen:

type Operation int

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

Empfohlen:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

Es gibt keine Einschränkungen, wo die Gruppierung verwendet werden soll. Sie können sie beispielsweise innerhalb einer Funktion verwenden:

Nicht empfohlen:

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

  ...
}

Empfohlen:

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

  ...
}

Ausnahme: Wenn Variablendeklarationen benachbart zu anderen Variablen, insbesondere innerhalb von Funktionslokalen Deklarationen, stehen, sollten sie zusammen gruppiert werden. Führen Sie dies auch für nicht zusammenhängende gemeinsam deklarierte Variablen durch.

Nicht empfohlen:

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

Empfohlen:

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

Import-Gruppierung

Imports sollten in zwei Kategorien gruppiert werden:

  • Standardbibliothek
  • Andere Bibliotheken

Standardmäßig ist dies die Gruppierung, die von goimports angewendet wird. Nicht empfohlen:

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

Empfohlen:

import (
  "fmt"
  "os"

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

Paketname

Beim Benennen eines Pakets sollten diese Regeln befolgt werden:

  • Alles in Kleinbuchstaben, keine Großbuchstaben oder Unterstriche.
  • In den meisten Fällen ist kein Umbenennen beim Importieren erforderlich.
  • Kurz und prägnant. Denken Sie daran, dass der Name überall, wo er verwendet wird, vollständig qualifiziert ist.
  • Vermeiden Sie Pluralformen. Verwenden Sie z. B. net/url anstelle von net/urls.
  • Vermeiden Sie die Verwendung von "common", "util", "shared" oder "lib". Diese sind nicht aussagekräftig genug.

Funktionenamen

Wir halten uns an die Go-Community-Konvention, Funktionenamen in MixedCaps zu verwenden. Eine Ausnahme wird für die Gruppierung von zusammenhängenden Testfällen gemacht, wo der Funktionsname Unterstriche enthalten kann, z.B.: TestMyFunction_WhatIsBeingTested.

Import-Aliase

Wenn der Paketname nicht mit dem letzten Element des Importpfads übereinstimmt, muss ein Import-Alias verwendet werden.

import (
  "net/http"

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

In allen anderen Fällen sollten Import-Aliase vermieden werden, es sei denn, es besteht ein direkter Konflikt zwischen den Imports. Nicht empfohlen:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

Empfohlen:

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

  nettrace "golang.net/x/trace"
)

Funktionengruppierung und -reihenfolge

  • Funktionen sollten grob in der Reihenfolge sortiert werden, in der sie aufgerufen werden.
  • Funktionen in derselben Datei sollten nach Empfängern gruppiert werden.

Daher sollten exportierte Funktionen zuerst in der Datei erscheinen, platziert nach den Definitionen von struct, const und var.

Ein newXYZ()/NewXYZ() darf nach den Typdefinitionen, aber vor den verbleibenden Methoden des Empfängers erscheinen.

Da Funktionen nach Empfängern gruppiert sind, sollten allgemeine Hilfsfunktionen am Ende der Datei erscheinen. Nicht empfohlen:

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

Empfohlen:

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

Verminderung der Verschachtelung

Der Code sollte die Verschachtelung reduzieren, indem Fehler-/Sonderfälle so früh wie möglich behandelt und entweder zurückgegeben oder die Schleife fortgesetzt wird. Die Reduzierung der Verschachtelung verringert die Menge des Codes auf mehreren Ebenen.

Nicht empfohlen:

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("Ungültiges v: %v", v)
  }
}

Empfohlen:

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

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

Unnötige else-Anweisung

Wenn eine Variable in beiden Zweigen eines if gesetzt wird, kann sie durch eine einzelne if-Anweisung ersetzt werden.

Nicht empfohlen:

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

Empfohlen:

a := 10
if b {
  a = 100
}

Deklaration von Variablen auf oberster Ebene

Auf oberster Ebene verwenden Sie das Standard-var-Schlüsselwort. Geben Sie den Typ nicht an, es sei denn, er unterscheidet sich vom Typ des Ausdrucks.

Nicht empfohlen:

var _s string = F()

func F() string { return "A" }

Empfohlen:

var _s = F()
// Da F explizit einen String-Typ zurückgibt, müssen wir den Typ für _s nicht explizit angeben

func F() string { return "A" }

Geben Sie den Typ an, wenn er nicht genau zum Typ des Ausdrucks passt.

type myError struct{}

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

func F() myError { return myError{} }

var _e error = F()
// F gibt eine Instanz des Typs myError zurück, aber wir brauchen den Typ error

Verwenden Sie '_' als Präfix für nicht exportierte Top-Level-Konstanten und Variablen

Für nicht exportierte Top-Level-Variablen und Konstanten muss diesen ein Unterstrich _ vorangestellt werden, um ihre globale Natur bei der Verwendung explizit anzuzeigen.

Grundlegende Begründung: Top-Level-Variablen und Konstanten haben einen Paketbereich. Die Verwendung generischer Namen kann leicht dazu führen, dass versehentlich der falsche Wert in anderen Dateien verwendet wird.

Nicht empfohlen:

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Standard Port", defaultPort)

  // Wir sehen keinen Kompilierfehler, wenn die erste Zeile von
  // Bar() gelöscht wird.
}

Empfohlen:

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Ausnahme: Nicht exportierte Fehlerwerte können das Präfix err ohne Unterstrich verwenden. Siehe Fehlerbenennung.

Einbetten in Strukturen

Eingebettete Typen (wie Mutex) sollten oben in der Feldliste innerhalb der Struktur platziert werden und zwischen den eingebetteten Feldern und den regulären Feldern muss eine leere Zeile stehen.

Nicht empfohlen:

type Client struct {
  version int
  http.Client
}

Empfohlen:

type Client struct {
  http.Client

  version int
}

Das Einbetten soll greifbare Vorteile bieten, wie das Hinzufügen oder Verbessern von Funktionen auf semantisch angemessene Weise. Es sollte ohne nachteiligen Einfluss auf den Benutzer verwendet werden. (Siehe auch: Vermeiden Sie das Einbetten von Typen in öffentlichen Strukturen)

Ausnahmen: Selbst in nicht exportierten Typen sollte Mutex nicht als eingebettetes Feld verwendet werden. Siehe auch: Der Nullwert von Mutex ist gültig.

Einbetten soll nicht:

  • ausschließlich aus ästhetischen oder praktischen Gründen existieren.
  • die Konstruktion oder Verwendung des äußeren Typs erschweren.
  • den Nullwert des äußeren Typs beeinflussen. Wenn der äußere Typ einen sinnvollen Nullwert hat, sollte auch nach dem Einbetten des inneren Typs ein sinnvoller Nullwert vorhanden sein.
  • die Nebenwirkung haben, nicht zusammenhängende Funktionen oder Felder des eingebetteten inneren Typs freizulegen.
  • nicht exportierte Typen freilegen.
  • die Klonform des äußeren Typs beeinflussen.
  • die API oder Typsemantik des äußeren Typs ändern.
  • den inneren Typ in einer nicht standardmäßigen Form einbetten.
  • Implementierungsdetails des äußeren Typs freilegen.
  • Benutzern ermöglichen, den internen Typ zu beobachten oder zu steuern.
  • das allgemeine Verhalten interner Funktionen auf eine Weise ändern, die Benutzer überraschen könnte.

Kurz gesagt: Betten Sie bewusst und zweckmäßig ein. Ein guter Lackmustest ist: "Werden all diese exportierten Methoden/Felder des inneren Typs direkt dem äußeren Typ hinzugefügt?" Wenn die Antwort einige oder nein lautet, betten Sie den inneren Typ nicht ein - verwenden Sie stattdessen Felder.

Nicht empfohlen:

type A struct {
    // Schlecht: A.Lock() und A.Unlock() sind jetzt verfügbar
    // Bietet keinen funktionalen Vorteil und ermöglicht es dem Benutzer, interne Details von A zu steuern.
    sync.Mutex
}

Empfohlen:

type countingWriteCloser struct {
    // Gut: Write() wird auf äußerer Ebene für einen bestimmten Zweck bereitgestellt
    // und delegiert die Arbeit an das Write() des inneren Typs.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

Lokale Variablendeklarationen

Wenn einer Variablen explizit ein Wert zugewiesen wird, sollte die Kurzvariablendeklarationsform (:=) verwendet werden.

Nicht empfohlen:

var s = "foo"

Empfohlen:

s := "foo"

Jedoch kann in einigen Fällen die Verwendung des var-Schlüsselworts für Standardwerte klarer sein.

Nicht empfohlen:

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

Empfohlen:

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

nil ist ein gültiger Slice

nil ist ein gültiger Slice mit einer Länge von 0, was bedeutet:

  • Sie sollten nicht explizit einen Slice mit einer Länge von Null zurückgeben. Stattdessen nil zurückgeben.

Nicht empfohlen:

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

Empfohlen:

if x == "" {
  return nil
}
  • Um zu überprüfen, ob ein Slice leer ist, verwenden Sie immer len(s) == 0 anstelle von nil.

Nicht empfohlen:

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

Empfohlen:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • Slices mit Nullwert (Slices, die mit var deklariert wurden) können sofort ohne Aufruf von make() verwendet werden.

Nicht empfohlen:

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

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

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

Empfohlen:

var nums []int

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

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

Denken Sie daran, dass obwohl ein nil-Slice ein gültiger Slice ist, er nicht gleich einem Slice mit einer Länge von 0 ist (der eine ist nil und der andere nicht), und sie können in verschiedenen Situationen unterschiedlich behandelt werden (z.B. Serialisierung).

Begrenzung des Variablenspektrums

Versuchen Sie, wenn möglich, den Gültigkeitsbereich von Variablen zu begrenzen, es sei denn, dies steht im Konflikt mit der Regel zur Reduzierung der Verzweigungstiefe.

Nicht empfohlen:

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

Empfohlen:

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

Wenn das Ergebnis eines Funktionsaufrufs außerhalb der if-Anweisung benötigt wird, sollte nicht versucht werden, den Gültigkeitsbereich zu begrenzen.

Nicht empfohlen:

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
}

Empfohlen:

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

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

fmt.Println(cfg)
return nil

Vermeidung von nackten Parametern

Unklare Parameter in Funktionsaufrufen können die Lesbarkeit beeinträchtigen. Wenn die Bedeutung der Parameternamen nicht offensichtlich ist, fügen Sie C-Style-Kommentare (/* ... */) zu den Parametern hinzu.

Nicht empfohlen:

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

printInfo("foo", true, true)

Empfohlen:

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

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

Für das obige Beispiel könnte ein besserer Ansatz sein, die bool-Typen durch benutzerdefinierte Typen zu ersetzen. Auf diese Weise könnte der Parameter in Zukunft potenziell mehr als nur zwei Zustände (true/false) unterstützen.

type Region int

const (
  UnbekannteRegion Region = iota
  Lokal
)

type Status int

const (
  StatusBereit Status= iota + 1
  StatusErledigt
  // Vielleicht werden wir in Zukunft auch einen StatusInProgress haben.
)

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

Verwenden von Raw String Literals zur Vermeidung von Escaping

Go unterstützt die Verwendung von Raw String Literals, die durch " ` " dargestellt werden, um Rohzeichenfolgen darzustellen. In Szenarien, in denen Escaping erforderlich ist, sollten wir diesen Ansatz verwenden, um die schwerer lesbaren manuell escaped Zeichenfolgen zu ersetzen.

Es kann sich über mehrere Zeilen erstrecken und Anführungszeichen enthalten. Die Verwendung dieser Zeichenfolgen kann schwerer lesbare manuell escaped Zeichenfolgen vermeiden.

Nicht empfohlen:

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

Empfohlen:

wantError := `unknown error:"test"`

Initialisierung von Strukturen

Initialisieren von Strukturen unter Verwendung von Feldnamen

Beim Initialisieren einer Struktur sollten Feldnamen fast immer angegeben werden. Dies wird derzeit von go vet durchgesetzt.

Nicht empfohlen:

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

Empfohlen:

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

Ausnahme: Bei 3 oder weniger Feldern können Feldnamen in Testtabellen ggf. ausgelassen werden.

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

Auslassen von Nullwertfeldern in Strukturen

Beim Initialisieren einer Struktur mit benannten Feldern sollten, sofern kein bedeutungsvoller Kontext vorliegt, Felder mit einem Nullwert ignoriert werden. Das heißt, wir setzen diese automatisch auf Nullwerte.

Nicht empfohlen:

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

Empfohlen:

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

Dies hilft, Lesehürden zu beseitigen, indem Standardwerte im Kontext ausgelassen werden. Geben Sie nur bedeutungsvolle Werte an.

Fügen Sie den Nullwert hinzu, wenn die Feldnamen einen bedeutungsvollen Kontext bieten. Zum Beispiel können Testfälle in einem tabellengesteuerten Test von der Benennung der Felder profitieren, auch wenn es sich um Nullwerte handelt.

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

Verwenden von var für Nullwertstrukturen

Wenn alle Felder einer Struktur bei der Deklaration ausgelassen werden, verwenden Sie var, um die Struktur zu deklarieren.

Nicht empfohlen:

user := User{}

Empfohlen:

var user User

Dies unterscheidet Nullwertstrukturen von solchen mit Feldern ohne Nullwert, ähnlich dem, was wir bevorzugen, wenn eine leere Slice deklariert wird.

Initialisierung von Strukturverweisen

Bei der Initialisierung von Strukturverweisen verwenden Sie &T{} anstelle von new(T), um die Konsistenz mit der Strukturinitialisierung sicherzustellen.

Nicht empfohlen:

sval := T{Name: "foo"}

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

Empfohlen:

sval := T{Name: "foo"}

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

Initialisieren von Maps

Verwenden Sie make(..), um eine leere Map zu initialisieren, die später programmgesteuert befüllt wird. Dies macht die Map-Initialisierung optisch anders als die Deklaration und ermöglicht auch die bequeme Hinzufügung von Größenhinweisen nach make.

Nicht empfohlen:

var (
  // m1 ist schreibgeschützt;
  // m2 löst einen Fehler bei der Schreiboperation aus
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

Empfohlen:

var (
  // m1 ist schreibgeschützt;
  // m2 löst einen Fehler bei der Schreiboperation aus
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

| Deklaration und Initialisierung sehen sehr ähnlich aus. | Deklaration und Initialisierung sehen sehr unterschiedlich aus. |

Geben Sie nach Möglichkeit die Größe der Map bei der Initialisierung an. Siehe Spezifizierung der Map-Größe für Details.

Darüber hinaus, wenn die Map eine feste Liste von Elementen enthält, verwenden Sie Map-Literale zur Initialisierung der Map.

Nicht empfohlen:

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

Empfohlen:

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

Die grundlegende Richtlinie ist, Map-Literale zur Hinzufügung einer festen Elementmenge während der Initialisierung zu verwenden. Andernfalls verwenden Sie make (und geben, wenn möglich, die Kapazität der Map an).

Zeichenkettenformat für Printf-ähnliche Funktionen

Wenn Sie die Formatzeichenkette einer Printf-ähnlichen Funktion außerhalb einer Funktion deklarieren, setzen Sie sie als const-Konstante.

Dies hilft go vet, eine statische Analyse der Formatzeichenkette durchzuführen.

Nicht empfohlen:

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

Empfohlen:

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

Benennung von Printf-Style Funktionen

Bei der Deklaration von Printf-Style Funktionen stellen Sie sicher, dass go vet den Formatstring erkennen und überprüfen kann.

Das bedeutet, dass Sie vordefinierte Printf-Style Funktionsnamen so weit wie möglich verwenden sollten. go vet wird diese standardmäßig überprüfen. Weitere Informationen finden Sie unter Printf Family.

Wenn vordefinierte Namen nicht verwendet werden können, enden Sie den ausgewählten Namen mit f: Wrapf anstelle von Wrap. go vet kann spezifische Printf-Style Namen zur Überprüfung anfordern, aber der Name muss mit f enden.

go vet -printfuncs=wrapf,statusf