Merge ~troyanov/maas:go-style-doc into maas:master

Proposed by Anton Troyanov
Status: Merged
Approved by: Anton Troyanov
Approved revision: 463577335b7421919d36a166a2ada8fc8078c9d7
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~troyanov/maas:go-style-doc
Merge into: maas:master
Diff against target: 855 lines (+849/-0)
1 file modified
go-style-guide.md (+849/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Alberto Donato (community) Approve
Review via email: mp+440865@code.launchpad.net

Commit message

docs: add go-style-guide

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b go-style-doc lp:~troyanov/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: fae663d616bbda67c71d6b2c671c57dc1b69db98

review: Approve
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

Overall, looks good, just a couple nits inline.

Revision history for this message
Alexsander de Souza (alexsander-souza) :
Revision history for this message
Anton Troyanov (troyanov) :
Revision history for this message
Alberto Donato (ack) :
Revision history for this message
Anton Troyanov (troyanov) :
~troyanov/maas:go-style-doc updated
a342645... by Anton Troyanov

fixup! docs: add go-style-guide

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b go-style-doc lp:~troyanov/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/2344/consoleText
COMMIT: a3426452f624125451709cad1ab5c5d69376cb5f

review: Needs Fixing
Revision history for this message
Anton Troyanov (troyanov) wrote :

jenkins: !test

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b go-style-doc lp:~troyanov/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: a3426452f624125451709cad1ab5c5d69376cb5f

review: Approve
~troyanov/maas:go-style-doc updated
4635773... by Anton Troyanov

fixup! fixup! docs: add go-style-guide

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b go-style-doc lp:~troyanov/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/2348/consoleText
COMMIT: 463577335b7421919d36a166a2ada8fc8078c9d7

review: Needs Fixing
Revision history for this message
Anton Troyanov (troyanov) wrote :

jenkins: !test

Revision history for this message
Alberto Donato (ack) wrote :

+1 nice work

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b go-style-doc lp:~troyanov/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 463577335b7421919d36a166a2ada8fc8078c9d7

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/go-style-guide.md b/go-style-guide.md
2new file mode 100644
3index 0000000..0cd51e0
4--- /dev/null
5+++ b/go-style-guide.md
6@@ -0,0 +1,849 @@
7+# MAAS Go style guide
8+
9+This document represents a set of idiomatic conventions in Go code that MAAS
10+contributors should follow. A lot of these are general guidelines for Go, while
11+others extend upon external resources and popular practices among Go community:
12+
13+1. [Effective Go](https://golang.org/doc/effective_go.html)
14+2. [Go Common Mistakes](https://github.com/golang/go/wiki/CommonMistakes)
15+3. [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
16+4. [Go Style Guide](https://google.github.io/styleguide/go/index#about)
17+5. [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
18+
19+This document is not exhaustive and will grow over time. Where the style guide
20+is contrary to common sense or there is a better way of doing things, it should
21+be discussed within MAAS team and the document should be updated accordingly.
22+
23+# WHY?
24+
25+Code can have a long lifetime; the effort to maintain and adapt it in the future
26+can be much larger than the original effort to produce the first version of it.
27+To keep the codebase readable and understandable, we must be consistent and
28+refer to a set of applicable conventions.
29+
30+Consistent code is easier to maintain, it requires less cognitive overhead, and
31+is easier to migrate or update as new conventions emerge or classes of bugs are
32+fixed.
33+
34+# HOW?
35+
36+This document addresses two main topics: [coding style](#style) and
37+[best practices](#best-practices). The former aims to ensure that our codebase
38+remains easy to read and understand, while the latter focuses on writing code
39+that is reliable and performs well.
40+
41+# Style
42+
43+## Naming conventions
44+
45+To a large extent we follow
46+[golang naming conventions](https://go.dev/doc/effective_go#names):
47+
48+- Names should strike a balance between concision and clarity, where for a local
49+ variable more weight might be put on concision while for an exported name
50+ clarity might have a larger weight.
51+
52+- Consistency is important in a somewhat large and long lived project, it is
53+ always a good idea to check whether there are similar entities or concepts in
54+ the code from which to borrow terminology or naming patterns, especially in
55+ the neighbourhood of the new code. For example when using a verb in a method
56+ name, it is good to check whether the verb is used for similar behaviour in
57+ other names or some other verb is more common for the usage.
58+
59+### Underscores
60+
61+Names in Go should in general not contain underscores. There are three
62+exceptions to this principle:
63+
64+- Package names that are only imported by generated code may contain
65+ underscores. See package names for more detail around how to choose multi-word
66+ package names.
67+
68+- Test, Benchmark and Example function names within `*_test.go` files may
69+ include underscores.
70+
71+- Low-level libraries that interoperate with the operating system or `cgo` may
72+ reuse identifiers, as is done in
73+ [syscall](https://pkg.go.dev/syscall#pkg-constants). This is expected to be
74+ very rare in most codebases.
75+
76+### Receiver names
77+
78+[Receiver](https://golang.org/ref/spec#Method_declarations) variable names must
79+be:
80+
81+- Short (usually one or two letters in length)
82+- Abbreviations for the type itself
83+- Applied consistently to every receiver for that type
84+
85+<table>
86+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
87+<tbody>
88+<tr><td>
89+
90+```go
91+func (tray Tray)
92+func (info *ResearchInfo)
93+func (this *ReportWriter)
94+func (self *Scanner)
95+```
96+
97+</td><td>
98+
99+```go
100+func (t Tray)
101+func (ri *ResearchInfo)
102+func (w *ReportWriter)
103+func (s *Scanner)
104+```
105+
106+</td></tr>
107+</tbody></table>
108+
109+### Constant names
110+
111+We follow the Go community's convention and use
112+[MixedCaps](https://google.github.io/styleguide/go/guide#mixed-caps).
113+[Exported](https://tour.golang.org/basics/3) constants start with uppercase,
114+while unexported constants start with lowercase.
115+
116+<table>
117+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
118+<tbody>
119+<tr><td>
120+
121+```go
122+const MAX_PACKET_SIZE = 512
123+const kMaxBufferSize = 1024
124+const KMaxUsersPergroup = 500
125+```
126+
127+</td><td>
128+
129+```go
130+const MaxPacketSize = 512
131+const (
132+ ExecuteBit = 1 << iota
133+ WriteBit
134+ ReadBit
135+)
136+```
137+
138+</td></tr>
139+</tbody></table>
140+
141+Name constants based on their role, not their values. If a constant does not
142+have a role apart from its value, then it is unnecessary to define it as a
143+constant.
144+
145+<table>
146+<thead><tr><th>Bad</th></tr></thead>
147+<tbody>
148+<tr><td>
149+
150+```go
151+const FortyTwo = 42
152+const (
153+ UserNameColumn = "username"
154+ GroupColumn = "group"
155+)
156+```
157+
158+</td></tr>
159+</tbody></table>
160+
161+### Function signatures
162+
163+We try to follow certain kind of ordering for parameters of functions and
164+methods:
165+
166+- `context.Context` if it makes sense for the function implementation
167+- Long lived/ambient objects
168+- The main entities the function or method operates on
169+- Any optional and ancillary parameters in some order of relevance
170+
171+Return parameters should be in some order of importance with `error` being last
172+as per Go conventions. Consistency is important, so parallel/similar
173+functions/methods should try to have the same/any shared parameters in the same
174+order.
175+
176+We do not recommend using named result parameters as
177+[naked returns](https://go.dev/tour/basics/7) can lead to bugs and also make
178+code harder to follow.
179+
180+<table>
181+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
182+<tbody>
183+<tr><td>
184+
185+```go
186+func (f *Foo) Temperature() (temp float64, err error) {
187+ ...
188+ ...
189+ if ... {
190+ // unclear what exactly is returned
191+ // also unclear what values of temp and err could be returned
192+ return
193+ }
194+ ...
195+}
196+```
197+
198+</td><td>
199+
200+```go
201+func (f *Foo) Temperature() (float64, error) {
202+ ...
203+ ...
204+ if ... {
205+ // here you have to be explicit with values returned
206+ // Otherwise you will get a compile-time error
207+ // ./prog.go: not enough return values
208+ // have ()
209+ // want (int, int)
210+ return 0, nil
211+ }
212+ ...
213+}
214+```
215+
216+</td></tr>
217+</tbody></table>
218+
219+### Getters
220+
221+Function and method names should not use a `Get` or `get` prefix, unless the
222+underlying concept uses the word “get” (e.g. an HTTP GET). Prefer starting the
223+name with the noun directly, for example use `Counts` over `GetCounts`.
224+
225+If the function involves performing a complex computation or executing a remote
226+call, a different word like `Compute` or `Fetch` can be used in place of `Get`,
227+to make it clear to a reader that the function call may take time and could
228+block or fail.
229+
230+### Repetition
231+
232+A piece of Go source code should avoid unnecessary repetition. One common source
233+of this is repetitive names, which often include unnecessary words or repeat
234+their context or type. Code itself can also be unnecessarily repetitive if the
235+same or a similar code segment appears multiple times in close proximity.
236+
237+<table>
238+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
239+<tbody>
240+<tr><td>
241+
242+```go
243+widget.NewWidget
244+widget.NewWidgetWithName
245+db.LoadFromDatabase
246+```
247+
248+</td><td>
249+
250+```go
251+widget.New
252+widget.NewWithName
253+db.Load
254+```
255+
256+</td></tr>
257+</tbody></table>
258+
259+The compiler always knows the type of a variable, and in most cases it is also
260+clear to the reader what type a variable is by how it is used. It is only
261+necessary to clarify the type of a variable if its value appears twice in the
262+same scope.
263+
264+<table>
265+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
266+<tbody>
267+<tr><td>
268+
269+```go
270+var numUsers int
271+var nameString string
272+var primaryProject *Project
273+```
274+
275+</td><td>
276+
277+```go
278+var users int
279+var name string
280+var primary *Project
281+```
282+
283+</td></tr>
284+</tbody></table>
285+
286+## Reduce Nesting
287+
288+Code should reduce nesting where possible by handling error cases/special
289+conditions first and returning early or continuing the loop. Reduce the amount
290+of code that is nested multiple levels.
291+
292+<table>
293+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
294+<tbody>
295+<tr><td>
296+
297+```go
298+for _, v := range data {
299+ if v.Foo == 1 {
300+ v = process(v)
301+ if err := v.Call(); err == nil {
302+ v.Send()
303+ } else {
304+ return err
305+ }
306+ } else {
307+ log.Printf("Invalid v: %v", v)
308+ }
309+}
310+```
311+
312+</td><td>
313+
314+```go
315+for _, v := range data {
316+ if v.Foo != 1 {
317+ log.Printf("Invalid v: %v", v)
318+ continue
319+ }
320+
321+ v = process(v)
322+ if err := v.Call(); err != nil {
323+ return err
324+ }
325+ v.Send()
326+}
327+```
328+
329+</td></tr>
330+</tbody></table>
331+
332+## Unnecessary Else
333+
334+If a variable is set in both branches of an `if`, it can be replaced with a
335+single `if`. However, this is not recommended when allocating memory or calling
336+heavy initializers. Note, this only when initializing the variable is cheap and
337+has no side-effects. (true for basic types, false when allocating memory or
338+calling heavy initializers)
339+
340+<table>
341+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
342+<tbody>
343+<tr><td>
344+
345+```go
346+var a int
347+
348+if b {
349+ a = 100
350+} else {
351+ a = 10
352+}
353+```
354+
355+</td><td>
356+
357+```go
358+a := 10
359+if b {
360+ a = 100
361+}
362+```
363+
364+</td></tr>
365+</tbody></table>
366+
367+## Local Variable Declarations
368+
369+Short variable declarations (`:=`) should be used if a variable is being set to
370+some value explicitly.
371+
372+<table>
373+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
374+<tbody>
375+<tr><td>
376+
377+```go
378+var s = "foo"
379+```
380+
381+</td><td>
382+
383+```go
384+s := "foo"
385+```
386+
387+</td></tr>
388+</tbody></table>
389+
390+However, the default value is clearer when the var keyword is used.
391+
392+<table>
393+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
394+<tbody>
395+<tr><td>
396+
397+```go
398+i := 0
399+t := Type{}
400+```
401+
402+</td><td>
403+
404+```go
405+var i int
406+var t Type
407+```
408+
409+</td></tr>
410+</tbody></table>
411+
412+### Declaring Empty Slices
413+
414+These two approaches on declaring a slice are functionally equivalent. Their
415+`len` and `cap` are both zero. But declaring a `nil` slice makes possible to
416+differentiate between a collection that doesn't exist and collection that is
417+empty.
418+
419+<table>
420+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
421+<tbody>
422+<tr><td>
423+
424+```go
425+s := []string{}
426+s == nil // false
427+```
428+
429+</td><td>
430+
431+```go
432+var s []string
433+s == nil // true
434+```
435+
436+</td></tr>
437+</tbody></table>
438+
439+## Comments
440+
441+Ideally all exported names should have doc comments for them following
442+[Go conventions](https://go.dev/doc/comment).
443+
444+Inline code comments should usually address non-obvious or unexpected parts of
445+the code. Repeating what the code does is not usually very informative.
446+
447+Code comments should:
448+
449+- Address the why something is done
450+- Clarify the more abstract impact of the low-level manipulation in the code
451+
452+It might be appropriate and useful also to give proper doc comments even to
453+complex unexported helpers.
454+
455+## Start Enums at One
456+
457+The standard way of introducing enumerations in Go is to declare a custom type
458+and a `const` group with `iota`. Since variables have a 0 default value, you
459+should usually start your enums on a non-zero value.
460+
461+<table>
462+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
463+<tbody>
464+<tr><td>
465+
466+```go
467+type Color int
468+
469+const (
470+ Red Color = iota
471+ Green
472+ Blue
473+)
474+
475+// Red=0, Green=1, Blue=2
476+```
477+
478+</td><td>
479+
480+```go
481+type Color int
482+
483+const (
484+ Red Color = iota + 1
485+ Green
486+ Blue
487+)
488+
489+// Red=1, Green=2, Blue=3
490+```
491+
492+</td></tr>
493+</tbody></table>
494+
495+There are cases where using the zero value makes sense, for example when the
496+zero value case is the desirable default behavior.
497+
498+```go
499+type LogOutput int
500+
501+const (
502+ LogToStdout LogOutput = iota
503+ LogToFile
504+ LogToRemote
505+
506+ LogDefault = LogToStdout
507+)
508+
509+// LogToStdout=0, LogToFile=1, LogToRemote=2
510+```
511+
512+# Best practices
513+
514+## Building strings
515+
516+Each time the `+` operator is used, Go creates a new string and copies the
517+contents of the previous strings into the new string, which can be
518+time-consuming and memory-intensive.
519+
520+So if you need to build a string in a loop, consider using
521+[strings.Builder](https://pkg.go.dev/strings#Builder)
522+
523+<table>
524+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
525+<tbody>
526+<tr><td>
527+
528+```go
529+var s string
530+
531+for i := 0; i < 10; i++ {
532+ s += strconv.Itoa(i)
533+}
534+```
535+
536+</td><td>
537+
538+```go
539+b = strings.Builder{}
540+
541+for i := 0; i < 10; i++ {
542+ builder.WriteString(strconv.Itoa(i))
543+}
544+
545+builder.String()
546+```
547+
548+</td></tr>
549+</tbody></table>
550+
551+## Prefer strconv over fmt
552+
553+When converting primitives to/from strings in a hot path,
554+[strconv](https://pkg.go.dev/strconv) is almost always faster than
555+[fmt](https://pkg.go.dev/fmt). When in doubt, do the benchmark.
556+
557+<table>
558+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
559+<tbody>
560+<tr><td>
561+
562+```go
563+for i := 0; i < b.N; i++ {
564+ s := fmt.Sprint(rand.Int())
565+}
566+```
567+
568+</td><td>
569+
570+```go
571+for i := 0; i < b.N; i++ {
572+ s := strconv.Itoa(rand.Int())
573+}
574+```
575+
576+</td></tr>
577+<tr><td>
578+
579+```plain
580+BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
581+```
582+
583+</td><td>
584+
585+```plain
586+BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
587+```
588+
589+</td></tr>
590+</tbody></table>
591+
592+## Prefer Specifying Container Capacity
593+
594+Specify container capacity where possible in order to allocate memory for the
595+container up front. This minimizes subsequent allocations (by copying and
596+resizing of the container) as elements are added.
597+
598+### Specifying Map Capacity Hints
599+
600+Where possible, provide capacity hints when initializing maps with `make()`.
601+
602+```go
603+make(map[T1]T2, hint)
604+```
605+
606+Providing a capacity hint to `make()` tries to right-size the map at
607+initialization time, which reduces the need for growing the map and allocations
608+as elements are added to the map.
609+
610+Note that, unlike slices, map capacity hints do not guarantee complete,
611+preemptive allocation, but are used to approximate the number of hashmap buckets
612+required. Consequently, allocations may still occur when adding elements to the
613+map, even up to the specified capacity.
614+
615+<table>
616+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
617+<tbody>
618+<tr><td>
619+
620+```go
621+// `m` is created without a size hint; there may be more
622+// allocations at assignment time.
623+m := make(map[string]os.FileInfo)
624+
625+files, _ := os.ReadDir("./files")
626+for _, f := range files {
627+ m[f.Name()] = f
628+}
629+```
630+
631+</td><td>
632+
633+```go
634+files, _ := os.ReadDir("./files")
635+
636+// `m` is created with a size hint; there may be fewer
637+// allocations at assignment time.
638+m := make(map[string]os.DirEntry, len(files))
639+for _, f := range files {
640+ m[f.Name()] = f
641+}
642+```
643+
644+</td></tr>
645+</tbody></table>
646+
647+### Specifying Slice Capacity
648+
649+Where possible, provide capacity hints when initializing slices with `make()`,
650+particularly when appending.
651+
652+```go
653+make([]T, length, capacity)
654+```
655+
656+Unlike maps, slice capacity is not a hint: the compiler will allocate enough
657+memory for the capacity of the slice as provided to `make()`, which means that
658+subsequent `append()` operations will incur zero allocations (until the length
659+of the slice matches the capacity, after which any appends will require a resize
660+to hold additional elements).
661+
662+<table>
663+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
664+<tbody>
665+<tr><td>
666+
667+```go
668+for n := 0; n < b.N; n++ {
669+ data := make([]int, 0)
670+ for k := 0; k < size; k++{
671+ data = append(data, k)
672+ }
673+}
674+```
675+
676+</td><td>
677+
678+```go
679+for n := 0; n < b.N; n++ {
680+ data := make([]int, 0, size)
681+ for k := 0; k < size; k++{
682+ data = append(data, k)
683+ }
684+}
685+```
686+
687+</td></tr>
688+<tr><td>
689+
690+```plain
691+BenchmarkBad-4 100000000 2.48s
692+```
693+
694+</td><td>
695+
696+```plain
697+BenchmarkGood-4 100000000 0.21s
698+```
699+
700+</td></tr>
701+</tbody></table>
702+
703+## Errors
704+
705+#TODO: errors wrap, error types, %w, errors chan
706+
707+## Testing
708+
709+Depending on a situation you might want to put your `*_test.go` files into
710+`foo_test` package (black-box testing) or `foo` package (white-box testing).
711+
712+Black-box testing will ensure you're only using the exported identifiers.
713+White-box Testing is ood for unit tests that require access to non-exported
714+variables, functions, and methods.
715+
716+We definitely prefer to write black-box tests, but there might be helpers and
717+unexported details that sometimes warrant testing, in which case we use
718+re-assignment or type aliasing in conventional `export_test.go` or
719+`export_foo_test.go` files in the package under test, to get access to what we
720+need to test. Here is a good
721+[example](https://go.dev/src/net/http/export_test.go) from the standard library.
722+This is usually needed if there is algorithmic complexity or error handling
723+behaviour that is hard to explore through the exported API, or is important to
724+illustrate the chosen behaviour of the helper in itself.
725+
726+#TODO: mocks (mockgen), testify, fixtures, integration tests
727+
728+## Table-driven tests
729+
730+Table-driven tests are easy to read and maintain. This format also makes it easy
731+to add or remove test cases, as well as modify existing ones.
732+
733+By using a single test function that takes input and expected output from a
734+table, you can avoid writing repetitive test code for each combination.
735+
736+While there are several techiques to declare test cases, the prefered way is to
737+use map literal syntax. While
738+
739+One advantage of using maps is that the "name" of each test case can simply be
740+the map key. But more importantly, map iteration order is undefined, hence
741+testing order doesn't impact results.
742+
743+```go
744+package main
745+
746+import (
747+ "testing"
748+
749+ "github.com/stretchr/testify/require"
750+)
751+
752+func TestFoo(t *testing.T) {
753+ testcases := map[string]struct {
754+ input string
755+ want string
756+ }{
757+ "test a": {input: "foo", want: "foo"},
758+ "test b": {input: "foo", want: "bar"},
759+ "test c": {input: "foo", want: "buz"},
760+ }
761+ for name, tc := range testcases {
762+ t.Run(name, func(t *testing.T) {
763+ got := Foo(tc.input)
764+ require.Equal(t, tc.want, got)
765+ })
766+ }
767+}
768+```
769+
770+## Handle Type Assertion Failures
771+
772+The single return value form of a
773+[type assertion](https://golang.org/ref/spec#Type_assertions) will panic on an
774+incorrect type. Therefore, always use the "comma ok" idiom.
775+
776+<table>
777+<thead><tr><th>Bad</th><th>Good</th></tr></thead>
778+<tbody>
779+<tr><td>
780+
781+```go
782+t := i.(string)
783+```
784+
785+</td><td>
786+
787+```go
788+t, ok := i.(string)
789+if !ok {
790+ // handle the error gracefully
791+}
792+```
793+
794+</td></tr>
795+</tbody></table>
796+
797+## Functional Options
798+
799+When it comes to creating an extensible API in Go, there are limited options
800+available. One common approach is to use a configuration struct that allows for
801+the addition of new fields in a backward-compatible manner. However, providing
802+default values for these fields might be confusing and passing around a
803+structure "for all options" is not perfect.
804+
805+Use this pattern for optional arguments in constructors and other public APIs
806+that you foresee needing to expand, especially if you already have three or more
807+arguments on those functions.
808+
809+```go
810+type Relay struct {
811+ server net.UDPAddr // DHCP server address
812+
813+ riface net.Interface
814+ riaddr net.IP
815+}
816+
817+type RelayOption func(r *Relay)
818+
819+func WithRemoteInterface(riface net.Interface, riaddr net.IP) RelayOption {
820+ return func(r *Relay) {
821+ r.riface = riface
822+ r.riaddr = riaddr
823+ }
824+}
825+
826+func NewRelay(server net.UDPAddr, opts ...RelayOption) *Relay {
827+ r := &Relay{
828+ server: server,
829+ }
830+
831+ for _, opt := range opts {
832+ opt(r)
833+ }
834+
835+ return r
836+}
837+
838+// ...
839+
840+func main() {
841+ relay := NewRelay(dhcpServer,
842+ WithRemoteInterface(...),
843+ )
844+}
845+```
846+
847+# Linting
848+
849+We use [golangci-lint](https://github.com/golangci/golangci-lint) which has
850+various linters available.
851+
852+Linters can catch most common issues and help to maintain code quality. However,
853+we should use linters judiciously and only enable those that are truly helpful
854+in improving code quality, rather than blindly enforcing rules that may not make
855+sense in a particular context.

Subscribers

People subscribed via source and target branches