Diff comments:
> diff --git a/go-style-guide.md b/go-style-guide.md > new file mode 100644 > index 0000000..13baa41 > --- /dev/null > +++ b/go-style-guide.md > @@ -0,0 +1,758 @@ > +# MAAS Go style guide > + > +This document represents a set of idiomatic conventions in Go code that MAAS > contributors should follow. A lot of these are general guidelines for Go, > while others extend upon external resources and popular practices among Go > community: > + > + > +1. [Effective Go](https://golang.org/doc/effective_go.html) > +2. [Go Common Mistakes](https://github.com/golang/go/wiki/CommonMistakes) > +3. [Go Code Review > Comments](https://github.com/golang/go/wiki/CodeReviewComments) > +4. [Go Style Guide](https://google.github.io/styleguide/go/index#about) > +5. [Uber Go Style > Guide](https://github.com/uber-go/guide/blob/master/style.md) > + > + > +This document is not exhaustive and will grow over time. Where the style > guide is contrary to common sense or there is a better way of doing things, > it should be discussed within MAAS team and the document should be updated > accordingly. > + > + > +# WHY? > + > +Code can have a long lifetime; the effort to maintain and adapt it in the > future can be much larger than the original effort to produce the first > version of it. To keep the codebase readable and understandable, we must be > consistent and refer to a set of applicable conventions. > + > +Consistent code is easier to maintain, it requires less cognitive overhead, > and is easier to migrate or update as new conventions emerge or classes of > bugs are fixed. > + > + > +# HOW? > + > +This document addresses two main topics: [coding style](#style) and [best > practices](#best-practices). > +The former aims to ensure that our codebase remains easy to read and > understand, while the latter focuses on writing code that is reliable and > performs well. > + > +# Style > + > +## Naming conventions > + > +To a large extent we follow [golang naming > conventions](https://go.dev/doc/effective_go#names): > + > +* Names should strike a balance between concision and clarity, where for a > local variable more weight might be put on concision while for an exported > name clarity might have a larger weight. > + > +* Consistency is important in a somewhat large and long lived project, it is > always a good idea to check whether there are similar entities or concepts in > the code from which to borrow terminology or naming patterns, especially in > the neighbourhood of the new code. For example when using a verb in a method > name, it is good to check whether the verb is used for similar behaviour in > other names or some other verb is more common for the usage. > + > + > +### Underscores > + > +Names in Go should in general not contain underscores. There are three > exceptions to this principle: > + > +* Package names that are only imported by generated code may contain > underscores. See package names for more detail around how to choose > multi-word package names. > + > +* Test, Benchmark and Example function names within `*_test.go` files may > include underscores. > + > +* Low-level libraries that interoperate with the operating system or `cgo` > may reuse identifiers, as is done in > [syscall](https://pkg.go.dev/syscall#pkg-constants). This is expected to be > very rare in most codebases. > + > + > +### Receiver names > + > +[Receiver](https://golang.org/ref/spec#Method_declarations) variable names > must be: > + > +* Short (usually one or two letters in length) > +* Abbreviations for the type itself > +* Applied consistently to every receiver for that type > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +func (tray Tray) > +func (info *ResearchInfo) > +func (this *ReportWriter) > +func (self *Scanner) > +``` > + > +</td><td> > + > +```go > +func (t Tray) > +func (ri *ResearchInfo) > +func (w *ReportWriter) > +func (s *Scanner) > +``` > + > +</td></tr> > +</tbody></table> > + > + > +### Constant names > + > +We follow the Go community's convention and use > [MixedCaps](https://google.github.io/styleguide/go/guide#mixed-caps). > [Exported](https://tour.golang.org/basics/3) constants start with uppercase, > while unexported constants start with lowercase. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +const MAX_PACKET_SIZE = 512 > +const kMaxBufferSize = 1024 > +const KMaxUsersPergroup = 500 > + > + > + > +``` > + > +</td><td> > + > +```go > +const MaxPacketSize = 512 > +const ( > + ExecuteBit = 1 << iota > + WriteBit > + ReadBit > +) > +``` > + > +</td></tr> > +</tbody></table> > + > +Name constants based on their role, not their values. If a constant does not > have a role apart from its value, then it is unnecessary to define it as a > constant. > + > +<table> > +<thead><tr><th>Bad</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +const FortyTwo = 42 > +const ( > + UserNameColumn = "username" > + GroupColumn = "group" > +) > +``` > + > +</td></tr> > +</tbody></table> > + > + > +### Function signatures > + > +We try to follow certain kind of ordering for parameters of functions and > methods: > + > +* `context.Context` if it makes sense for the function implementation > +* Long lived/ambient objects > +* The main entities the function or method operates on > +* Any optional and ancillary parameters in some order of relevance > + > +Return parameters should be in some order of importance with `error` being > last as per Go conventions. > +Consistency is important, so parallel/similar functions/methods should try > to have the same/any shared parameters in the same order. > + > + > +We do not recommend using named result parameters as [naked > returns](https://go.dev/tour/basics/7) can lead to bugs and also make code > harder to follow. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +func (f *Foo) Temperature() (temp float64, err error) +1, I think named returns are a convenient way to declare variables. Just update the example to show the problematic `return` statement. > +``` > + > +</td><td> > + > +```go > +func (f *Foo) Temperature() (float64, error) > +``` > + > +</td></tr> > +</tbody></table> > + > + > +### Getters > + > +Function and method names should not use a `Get` or `get` prefix, unless the > underlying concept uses the word “get” (e.g. an HTTP GET). Prefer starting > the name with the noun directly, for example use `Counts` over `GetCounts`. > + > +If the function involves performing a complex computation or executing a > remote call, a different word like `Compute` or `Fetch` can be used in place > of `Get`, to make it clear to a reader that the function call may take time > and could block or fail. > + > +### Repetition > + > +A piece of Go source code should avoid unnecessary repetition. One common > source of this is repetitive names, which often include unnecessary words or > repeat their context or type. Code itself can also be unnecessarily > repetitive if the same or a similar code segment appears multiple times in > close proximity. > + > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +widget.NewWidget > +widget.NewWidgetWithName > +db.LoadFromDatabase > +``` > + > +</td><td> > + > +```go > +widget.New > +widget.NewWithName > +db.Load > +``` > + > +</td></tr> > +</tbody></table> > + > +The compiler always knows the type of a variable, and in most cases it is > also clear to the reader what type a variable is by how it is used. It is > only necessary to clarify the type of a variable if its value appears twice > in the same scope. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +var numUsers int > +var nameString string > +var primaryProject *Project > +``` > + > +</td><td> > + > +```go > +var users int > +var name string > +var primary *Project > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Reduce Nesting > + > +Code should reduce nesting where possible by handling error cases/special > conditions first and returning early or continuing the loop. Reduce the > amount of code that is nested multiple levels. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +for _, v := range data { > + if v.Foo == 1 { > + v = process(v) > + if err := v.Call(); err == nil { > + v.Send() > + } else { > + return err > + } > + } else { > + log.Printf("Invalid v: %v", v) > + } > +} > +``` > + > +</td><td> > + > +```go > +for _, v := range data { > + if v.Foo != 1 { > + log.Printf("Invalid v: %v", v) > + continue > + } > + > + v = process(v) > + if err := v.Call(); err != nil { > + return err > + } > + v.Send() > +} > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Unnecessary Else > + > +If a variable is set in both branches of an `if`, it can be replaced with a > single `if`. only when initializing the variable is cheap and has no side-effects. (true for basic types, false when allocating memory or calling heavy initializers) > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +var a int > + > +if b { > + a = 100 > +} else { > + a = 10 > +} > +``` > + > +</td><td> > + > +```go > +a := 10 > +if b { > + a = 100 > +} > + > + > + > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Local Variable Declarations > + > +Short variable declarations (`:=`) should be used if a variable is being set > to some value explicitly. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +var s = "foo" > +``` > + > +</td><td> > + > +```go > +s := "foo" > +``` > + > +</td></tr> > +</tbody></table> > + > +However, there are cases where the default value is clearer when the var > keyword is used e.g. declaring empty slices. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +func f(list []int) { > + filtered := []int{} > + for _, v := range list { > + if v > 10 { > + filtered = append(filtered, v) > + } > + } > +} > +``` > + > +</td><td> > + > +```go > +func f(list []int) { > + var filtered []int > + for _, v := range list { > + if v > 10 { > + filtered = append(filtered, v) > + } > + } > +} > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Comments > + > +Ideally all exported names should have doc comments for them following [Go > conventions](https://go.dev/doc/comment). > + > +Inline code comments should usually address non-obvious or unexpected parts > of the code. Repeating what the code does is not usually very informative. > + > +Code comments should: > + > +* Address the why something is done > +* Clarify the more abstract impact of the low-level manipulation in the code > + > +It might be appropriate and useful also to give proper doc comments even to > complex unexported helpers. > + > +## Start Enums at One > + > +The standard way of introducing enumerations in Go is to declare a custom > type > +and a `const` group with `iota`. Since variables have a 0 default value, you > +should usually start your enums on a non-zero value. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +type Color int > + > +const ( > + Red Color = iota > + Green > + Blue > +) > + > +// Red=0, Green=1, Blue=2 > +``` > + > +</td><td> > + > +```go > +type Color int > + > +const ( > + Red Color = iota + 1 > + Green > + Blue > +) > + > +// Red=1, Green=2, Blue=3 > +``` > + > +</td></tr> > +</tbody></table> > + > +There are cases where using the zero value makes sense, for example when the > +zero value case is the desirable default behavior. > + > +```go > +type LogOutput int > + > +const ( > + LogToStdout LogOutput = iota > + LogToFile > + LogToRemote I often find useful to add an alias to the default value as the last value of the enumeration, e.g. `LogDefault = LogToStdout`. This makes explicit this enum has a default value by design, and in some cases it helps to make the intention clear. e.g. func (Config *c) Reset() { c.logOutput = LogDefault // restore the default behavior (whatever it is) } > +) > + > +// LogToStdout=0, LogToFile=1, LogToRemote=2 > +``` > + > +# Best practices > + > +## Building strings > + > +Each time the `+` operator is used, Go creates a new string and copies the > contents of the previous strings into the new string, which can be > time-consuming and memory-intensive. > + > +So if you need to build a string in a loop, consider using > [strings.Builder](https://pkg.go.dev/strings#Builder) > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +var s string > + > +for i := 0; i < 10; i++ { > + s += strconv.Itoa(i) > +} > + > + > +``` > + > +</td><td> > + > +```go > +b = strings.Builder{} > + > +for i := 0; i < 10; i++ { > + builder.WriteString(strconv.Itoa(i)) > +} > + > +builder.String() > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Prefer strconv over fmt > + > +When converting primitives to/from strings in a hot path, > [strconv](https://pkg.go.dev/strconv) is almost always faster than > [fmt](https://pkg.go.dev/fmt). When in doubt, do the benchmark. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +for i := 0; i < b.N; i++ { > + s := fmt.Sprint(rand.Int()) > +} > +``` > + > +</td><td> > + > +```go > +for i := 0; i < b.N; i++ { > + s := strconv.Itoa(rand.Int()) > +} > +``` > + > +</td></tr> > +<tr><td> > + > +```plain > +BenchmarkFmtSprint-4 143 ns/op 2 allocs/op > +``` > + > +</td><td> > + > +```plain > +BenchmarkStrconv-4 64.2 ns/op 1 allocs/op > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Prefer Specifying Container Capacity > + > +Specify container capacity where possible in order to allocate memory for the > +container up front. This minimizes subsequent allocations (by copying and > +resizing of the container) as elements are added. > + > +### Specifying Map Capacity Hints > + > +Where possible, provide capacity hints when initializing > +maps with `make()`. > + > +```go > +make(map[T1]T2, hint) > +``` > + > +Providing a capacity hint to `make()` tries to right-size the > +map at initialization time, which reduces the need for growing > +the map and allocations as elements are added to the map. > + > +Note that, unlike slices, map capacity hints do not guarantee complete, > +preemptive allocation, but are used to approximate the number of hashmap > buckets > +required. Consequently, allocations may still occur when adding elements to > the > +map, even up to the specified capacity. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +// `m` is created without a size hint; there may be more > +// allocations at assignment time. > +m := make(map[string]os.FileInfo) > + > +files, _ := os.ReadDir("./files") > +for _, f := range files { > + m[f.Name()] = f > +} > +``` > + > +</td><td> > + > +```go > +files, _ := os.ReadDir("./files") > + > +// `m` is created with a size hint; there may be fewer > +// allocations at assignment time. > +m := make(map[string]os.DirEntry, len(files)) > +for _, f := range files { > + m[f.Name()] = f > +} > +``` > + > +</td></tr> > +</tbody></table> > + > +### Specifying Slice Capacity > + > +Where possible, provide capacity hints when initializing slices with > `make()`, > +particularly when appending. > + > +```go > +make([]T, length, capacity) > +``` > + > +Unlike maps, slice capacity is not a hint: the compiler will allocate enough > +memory for the capacity of the slice as provided to `make()`, which means > that > +subsequent `append()` operations will incur zero allocations (until the > length > +of the slice matches the capacity, after which any appends will require a > resize > +to hold additional elements). > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +for n := 0; n < b.N; n++ { > + data := make([]int, 0) > + for k := 0; k < size; k++{ > + data = append(data, k) > + } > +} > +``` > + > +</td><td> > + > +```go > +for n := 0; n < b.N; n++ { > + data := make([]int, 0, size) > + for k := 0; k < size; k++{ > + data = append(data, k) > + } > +} > +``` > + > +</td></tr> > +<tr><td> > + > +```plain > +BenchmarkBad-4 100000000 2.48s > +``` > + > +</td><td> > + > +```plain > +BenchmarkGood-4 100000000 0.21s > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Errors > + > +#TODO: errors wrap, error types, %w, errors chan > + > + > +## Testing > + > +Depending on a situation you might want to put your `*_test.go` files into > `foo_test` package (black-box testing) or `foo` package (white-box testing). > + > +Black-box testing will ensure you're only using the exported identifiers. > +White-box Testing is ood for unit tests that require access to non-exported > variables, functions, and methods. > + > +We definitely prefer to write black-box tests, but there might be helpers > and unexported details that sometimes warrant testing, in which case we use > re-assignment or type aliasing in conventional `export_test.go` or > `export_foo_test.go` files in the package under test, to get access to what > we need to test. Here is a good > [example](https://go.dev/src/net/http/export_test.go) from the standard > library. This is usually needed if there is algorithmic complexity or error > handling behaviour that is hard to explore through the exported API, or is > important to illustrate the chosen behaviour of the helper in itself. > + > +#TODO: mocks (mockgen), testify, fixtures, integration tests > + > + > +## Table-driven tests > + > +Table-driven tests are easy to read and maintain. This format also makes it > easy to add or remove test cases, as well as modify existing ones. > + > +By using a single test function that takes input and expected output from a > table, you can avoid writing repetitive test code for each combination. > + > +While there are several techiques to declare test cases, the prefered way is > to use map literal syntax. > +While > + > +One advantage of using maps is that the "name" of each test case can simply > be the map key. But more importantly, map iteration order is undefined, hence > testing order doesn't impact results. > + > + > +```go > +package main > + > +import ( > + "testing" > + > + "github.com/stretchr/testify/require" > +) > + > +func TestFoo(t *testing.T) { > + testcases := map[string]struct { > + input string > + want string > + }{ > + "test a": {input: "foo", want: "foo"}, > + "test b": {input: "foo", want: "bar"}, > + "test c": {input: "foo", want: "buz"}, > + } > + for name, tc := range testcases { > + t.Run(name, func(t *testing.T) { > + got := Foo(tc.input) > + require.Equal(t, tc.want, got) > + }) > + } > +} > +``` > + > +## Handle Type Assertion Failures > + > +The single return value form of a [type > assertion](https://golang.org/ref/spec#Type_assertions) will panic on an > incorrect type. Therefore, always use the "comma ok" idiom. > + > +<table> > +<thead><tr><th>Bad</th><th>Good</th></tr></thead> > +<tbody> > +<tr><td> > + > +```go > +t := i.(string) > + > + > + > +``` > + > +</td><td> > + > +```go > +t, ok := i.(string) > +if !ok { > + // handle the error gracefully > +} > +``` > + > +</td></tr> > +</tbody></table> > + > + > +## Functional Options > + > +When it comes to creating an extensible API in Go, there are limited options > available. One common approach is to use a configuration struct that allows > for the addition of new fields in a backward-compatible manner. However, > providing default values for these fields might be confusing and passing > around a structure "for all options" is not perfect. > + > +Use this pattern for optional arguments in constructors and other public > APIs that you foresee needing to expand, especially if you already have three > or more arguments on those functions. > + > +```go > +type Relay struct { > + server net.UDPAddr // DHCP server address > + > + riface net.Interface > + riaddr net.IP > +} > + > +type RelayOption func(r *Relay) > + > +func WithRemoteInterface(riface net.Interface, riaddr net.IP) RelayOption { > + return func(r *Relay) { > + r.riface = riface > + r.riaddr = riaddr > + } > +} > + > +func NewRelay(server net.UDPAddr, opts ...RelayOption) *Relay { > + r := &Relay{ > + server: server, > + } > + > + for _, opt := range opts { > + opt(r) > + } > + > + return r > +} > + > +// ... > + > +func main() { > + relay := NewRelay(dhcpServer, > + WithRemoteInterface(...), > + ) > +} > +``` > + > + > +# Linting > + > +We use [golangci-lint](https://github.com/golangci/golangci-lint) which has > various linters available. > + > +Linters can catch most common issues and help to maintain code quality. > However, we should use linters judiciously and only enable those that are > truly helpful in improving code quality, rather than blindly enforcing rules > that may not make sense in a particular context. > + -- https://code.launchpad.net/~troyanov/maas/+git/maas/+merge/440865 Your team MAAS Committers is subscribed to branch maas:master. -- Mailing list: https://launchpad.net/~sts-sponsors Post to : [email protected] Unsubscribe : https://launchpad.net/~sts-sponsors More help : https://help.launchpad.net/ListHelp

