Grundlegende Richtlinien für den Golang-Codierungsstandard

Verwenden von defer zum Freigeben von Ressourcen

Verwenden Sie defer, um Ressourcen wie Dateien und Sperren freizugeben.

Nicht empfohlen:

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// Es ist einfach, das Entsperren zu vergessen, wenn es mehrere Rückkehrzweige gibt

Empfohlen:

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// Lesbarer

Der Overhead von defer ist äußerst gering, daher sollte er nur vermieden werden, wenn nachgewiesen werden kann, dass die Ausführungszeit der Funktion im Nanosekundenbereich liegt. Die Verwendung von defer zur Verbesserung der Lesbarkeit ist sinnvoll, da die Kosten für ihre Verwendung vernachlässigbar sind. Dies gilt insbesondere für größere Methoden, die mehr als nur einfachen Speicherzugriff umfassen, bei denen der Ressourcenverbrauch anderer Berechnungen den von defer bei weitem übertrifft.

Die Kanalgröße sollte 1 oder ungepuffert sein

Kanäle sollten in der Regel eine Größe von 1 haben oder ungepuffert sein. Standardmäßig sind Kanäle ungepuffert und haben eine Größe von null. Jede andere Größe muss streng überprüft werden. Dabei muss berücksichtigt werden, wie die Größe bestimmt wird, was das Schreiben an den Kanal unter hoher Last verhindert und bei Blockierung verändert wird, und welche Änderungen in der Systemlogik dabei auftreten.

Nicht empfohlen:

// Sollte ausreichen, um jede Situation zu bewältigen!
c := make(chan int, 64)

Empfohlen:

// Größe: 1
c := make(chan int, 1) // oder
// Ungepufferter Kanal, Größe ist 0
c := make(chan int)

Enums beginnen bei 1

Die Standardmethode zum Einführen von Enums in Go besteht darin, einen benutzerdefinierten Typ und eine const-Gruppe zu deklarieren, die iota verwendet. Da der Standardwert von Variablen 0 ist, sollten Enums in der Regel mit einem nicht-nullwert beginnen.

Nicht empfohlen:

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2

Empfohlen:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

In einigen Fällen ergibt es Sinn, den Nullwert zu verwenden (Enums, die bei null beginnen), zum Beispiel, wenn der Nullwert das ideale Standardverhalten ist.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

Verwenden von atomaren Operationen

Verwenden Sie atomare Operationen aus dem Paket sync/atomic für den Umgang mit primitiven Typen (int32, int64, usw.), da es leicht ist, die Verwendung atomarer Operationen zum Lesen oder Ändern von Variablen zu vergessen.

go.uber.org/atomic fügt diesen Operationen Typsicherheit hinzu, indem der zugrunde liegende Typ verborgen wird. Zusätzlich enthält es einen praktischen Typ atomic.Bool.

Nicht empfohlener Ansatz:

type foo struct {
  running int32  // atomar
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // bereits in Betrieb...
     return
  }
  // starten des Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // Wettlaufbedingung!
}

Empfohlener Ansatz:

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // bereits in Betrieb...
     return
  }
  // starten des Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

Vermeiden Sie veränderliche Globale Variablen

Verwenden Sie den Dependency-Injection-Ansatz, um die Veränderung globaler Variablen zu vermeiden. Dies gilt sowohl für Funktionszeiger als auch für andere Werttypen.

Nicht empfohlener Ansatz 1:

// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}

Empfohlener Ansatz 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)
}

Nicht empfohlener Ansatz 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))
}

Empfohlener Ansatz 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))
}

Vermeiden Sie die Verwendung vordeklarierter Bezeichner

Die Go-Sprachspezifikation enthält mehrere vordeklarierte Bezeichner, die in Go-Projekten nicht verwendet werden sollten. Diese vordeklarierten Bezeichner sollten in verschiedenen Kontexten nicht als Namen wiederverwendet werden, da dies die ursprünglichen Bezeichner im aktuellen Bereich (oder einem verschachtelten Bereich) verdecken und möglicherweise zu Code-Verwirrung führen kann. Im besten Fall gibt der Compiler einen Fehler aus; im schlimmsten Fall kann ein solcher Code potenzielle, schwerwiegende Fehler einführen.

Nicht empfohlene Praxis 1:

var error string
// `error` verdeckt implizit den integrierten Bezeichner

// oder

func handleErrorMessage(error string) {
    // `error` verdeckt implizit den integrierten Bezeichner
}

Empfohlene Praxis 1:

var errorMessage string
// `error` verweist nun auf den nicht verdeckten integrierten Bezeichner

// oder

func handleErrorMessage(msg string) {
    // `error` verweist nun auf den nicht verdeckten integrierten Bezeichner
}

Nicht empfohlene Praxis 2:

type Foo struct {
    // Obwohl diese Felder technisch gesehen nicht verdecken, wird das Neudefinieren von `error` oder `string` Strings jetzt mehrdeutig.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` und `f.error` erscheinen visuell ähnlich
    return f.error
}

func (f Foo) String() string {
    // `string` und `f.string` erscheinen visuell ähnlich
    return f.string
}

Empfohlene Praxis 2:

type Foo struct {
    // `error` und `string` sind jetzt explizit.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

Beachten Sie, dass der Compiler keine Fehler generiert, wenn vordeklarierte Bezeichner verwendet werden, aber Tools wie go vet weisen korrekterweise auf diese und andere implizit verwandte Probleme hin.

Vermeiden Sie die Verwendung von init()

Versuchen Sie, die Verwendung von init() so weit wie möglich zu vermeiden. Wenn init() unvermeidbar oder bevorzugt ist, sollte der Code versuchen:

  1. Die Vollständigkeit unabhängig von der Programmumgebung oder dem Aufruf sicherzustellen.
  2. Sich nicht auf die Reihenfolge oder Seiteneffekte anderer init()-Funktionen zu verlassen. Obwohl die Reihenfolge von init() explizit ist, kann sich der Code ändern, wodurch die Beziehung zwischen init()-Funktionen den Code fragil und fehleranfällig machen kann.
  3. Den Zugriff auf globale oder Umweltzustände wie Maschineninformationen, Umgebungsvariablen, Arbeitsverzeichnisse, Programmparameter/Eingaben usw. vermeiden.
  4. E/A-Vorgänge, einschließlich Dateisystemen, Netzwerken und Systemaufrufen, vermeiden.

Code, der diese Anforderungen nicht erfüllt, gehört möglicherweise zum main()-Aufruf (oder an anderer Stelle im Programmlauf) oder wird als Teil von main() selbst geschrieben. Insbesondere Bibliotheken, die von anderen Programmen verwendet werden sollen, sollten besonders auf Vollständigkeit achten, anstatt "Init-Magie" auszuführen.

Nicht empfohlener Ansatz 1:

type Foo struct {
    // ...
}
var _defaultFoo Foo
func init() {
    _defaultFoo = Foo{
        // ...
    }
}

Empfohlener Ansatz 1:

var _defaultFoo = Foo{
    // ...
}
// oder, für bessere Testbarkeit:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

Nicht empfohlener Ansatz 2:

type Config struct {
    // ...
}
var _config Config
func init() {
    // Schlecht: basierend auf dem aktuellen Verzeichnis
    cwd, _ := os.Getwd()
    // Schlecht: E/A
    raw, _ := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

Empfohlener Ansatz 2:

type Config struct {
    // ...
}
func loadConfig() Config {
    cwd, err := os.Getwd()
    // Fehler behandeln
    raw, err := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // Fehler behandeln
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

Unter Berücksichtigung der obigen Überlegungen kann in einigen Fällen init() bevorzugter oder notwendiger sein, einschließlich:

  • Kann nicht als einzelne Zuweisung eines komplexen Ausdrucks dargestellt werden.
  • Einfügbare Hooks wie database/sql, Typenregistrierungen usw.

Bevorzugen Sie das Angeben der Slice-Kapazität beim Anhängen

Bevorzugen Sie bei der Initialisierung einer Slice, die angehängt werden soll, immer die Angabe eines Kapazitätswertes für make().

Nicht empfohlener Ansatz:

for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

Empfohlener Ansatz:

for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

Verwendung von Feld-Tags bei der Strukturserialisierung

Bei der Serialisierung in JSON, YAML oder einem anderen Format, das Feldnamen auf der Grundlage von Tags unterstützt, sollten entsprechende Tags zur Annotation verwendet werden.

Nicht empfohlen:

type Stock struct {
  Price int
  Name  string
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

Empfohlen:

type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

In der Theorie ist das Serialisierungsformat einer Struktur ein Vertrag zwischen verschiedenen Systemen. Änderungen am Serialisierungsformat der Struktur (einschließlich Feldnamen) brechen diesen Vertrag. Durch die Angabe von Feldnamen in Tags wird der Vertrag explizit gemacht und hilft auch dabei, versehentliche Verstöße gegen den Vertrag durch Refactoring oder Umbenennen von Feldern zu verhindern.