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:
- Die Vollständigkeit unabhängig von der Programmumgebung oder dem Aufruf sicherzustellen.
- Sich nicht auf die Reihenfolge oder Seiteneffekte anderer
init()
-Funktionen zu verlassen. Obwohl die Reihenfolge voninit()
explizit ist, kann sich der Code ändern, wodurch die Beziehung zwischeninit()
-Funktionen den Code fragil und fehleranfällig machen kann. - Den Zugriff auf globale oder Umweltzustände wie Maschineninformationen, Umgebungsvariablen, Arbeitsverzeichnisse, Programmparameter/Eingaben usw. vermeiden.
- 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.