Skip to content

Commit 4f8550c

Browse files
committed
Tech stack: Go: add Interfaces section
1 parent a6e05ae commit 4f8550c

File tree

2 files changed

+111
-0
lines changed

2 files changed

+111
-0
lines changed

dictionary.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,16 @@ SDK
8888
semver
8989
skillset
9090
Slackbot
91+
struct
92+
subpackage
9193
subpackages
9294
subtest
9395
subtests
9496
Sygma
9597
TBD
9698
TODO
9799
Udemy
100+
unexported
98101
unintuitive
99102
unpatched
100103
USD

docs/2_development/2_tech-stack/go.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,114 @@ jobs:
325325
Make sure to pin the linter version (`version: v1.45`) since the same linters can behave differently from a version to another.
326326
:::
327327

328+
## Interfaces
329+
330+
Go interfaces are useful for two main reasons:
331+
332+
1. Abstract implementation details
333+
2. Test mocks generation (see the [Mocking section](#mocking))
334+
335+
There are three rules to define interfaces and enforce good code quality, as described in the subsections below:
336+
337+
### Accept interfaces, return concrete types
338+
339+
This is by far the most important and fruitful rule.
340+
When returning something meant to be used as an interface, always return a pointer.
341+
342+
For example:
343+
344+
```go
345+
package database
346+
347+
type Database struct {}
348+
349+
func New() *Database {
350+
return &Database{}
351+
}
352+
353+
func (d *Database) Get(key string) (value string) {
354+
// ...
355+
}
356+
357+
func (d *Database) Set(key string, value string) {
358+
// ...
359+
}
360+
```
361+
362+
For callers, they should define **their own interface in their own package** (and **NOT import them** from another package).
363+
Continuing our example:
364+
365+
```go
366+
package service
367+
368+
type Database interface {
369+
Get(key string) (value string)
370+
Set(key string, value string)
371+
}
372+
373+
type Service struct {
374+
database Database
375+
}
376+
377+
func New(database Database) *Service {
378+
return &Service{database: database}
379+
}
380+
```
381+
382+
An additional element to note is that you should inject the actual implementation down your call stack.
383+
Implementation initialization should ideally occur at the top-most layer of your call stack, such as the `main.main` function.
384+
385+
Finally, to enforce such rule, you can use the [`ireturn` linter](https://github.com/butuzov/ireturn) in your [.golangci.yml configuration](#linting).
386+
387+
### Narrow interfaces and composition
388+
389+
All your interfaces should be as **narrow** as possible.
390+
391+
Interfaces should only have methods that must be used in your **production code** of your Go package.
392+
For example if you need only a single method and the concrete implementation has 2 methods, your interface should only contain that single method.
393+
As a side effect, this also produces smaller generated mock test file.
394+
395+
If you need to call a method on an interface in **test code** but it's not needed in production code, **DO NOT ADD THE METHOD TO THE INTERFACE**.
396+
Instead do a type assertion on the interface to call the method. For example:
397+
398+
```go
399+
value := myInterface.(*myImplementation).GetValue()
400+
```
401+
402+
Interfaces should have one or two methods. If you need a larger interface, compose it using smaller interfaces. For example:
403+
404+
```go
405+
type Database interface {
406+
Getter
407+
Setter
408+
}
409+
410+
type Getter interface {
411+
Get(key string) (value string)
412+
}
413+
414+
type Setter interface {
415+
Set(key string, value string)
416+
}
417+
```
418+
419+
This is also useful such that you can use the narrow interfaces (such as `Getter`) in unexported package-local functions.
420+
421+
### Exported interfaces with exported methods only
422+
423+
All your interfaces and their methods should be **EXPORTED**. This forces you to either:
424+
425+
* dependency inject the concrete implementation, which would force you to export the interface
426+
* test the actual implementation if it's defined in the same package (you shouldn't interface & mock it), which would force you to remove the interface definition.
427+
* split out the implementation in its own subpackage and then define your exported interface with exported methods.
428+
429+
A side note is that `mockgen` is rather terrible at generating mocks from interfaces with unexported methods, so that's something you can avoid by having exported methods.
430+
431+
### Importing interfaces
432+
433+
As a general rule, interface should **NOT** be imported in order to have narrower interfaces, decouple code and reduce package dependency contention.
434+
The exception is to import interfaces from the **standard library**, some commonly imported interfaces are for example: `io.Reader`, `io.Writer`, `io.Closer`, `fmt.Stringer`, `rand.Source`, `sort.Interface`, `http.Handler`.
435+
328436
## Panic
329437

330438
In Go, `panic` should only be used when a **programming error** has been encountered.

0 commit comments

Comments
 (0)