bahlo / go-styleguide
- четверг, 3 августа 2017 г. в 03:14:07
Opinionated Styleguide for the Go language
This serves as a supplement to Effective Go, based on years of experience and inspiration/ideas from conference talks.
Don't:
file, err := os.Open("foo.txt")
if err != nil {
return err
}
Using the approach above can lead to unclear error messages because of missing context.
Do:
import "github.com/pkg/errors" // for example
// ...
file, err := os.Open("foo.txt")
if err != nil {
return errors.Wrap(err, "open foo.txt failed")
}
Wrapping errors with a custom message provides context as it gets propagated up the stack. This does not always make sense. If you're unsure if the context of a returned error is at all times sufficient, wrap it. Make sure the root error is still accessible somehow for type checking.
Use dep, since it's production ready and will soon become part of the toolchain. – Sam Boyer at GopherCon 2017
Since dep
can handle versions, tag your packages using
Semantic Versioning.
While gopkg.in is a great tool and was really
useful, it tags one version and is not meant to work with dep
.
Prefer direct import and specify version in Gopkg.toml
.
Don't:
log.Printf("Listening on :%d", port)
http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
// 2017/07/29 13:05:50 Listening on :80
Do:
import "github.com/uber-go/zap" // for example
// ...
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("Server started",
zap.Int("port", port),
zap.String("env", env),
)
http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
// {"level":"info","ts":1501326297.511464,"caller":"Desktop/structured.go:17","msg":"Server started","port":80,"env":"production"}
This is a harmless example, but using structured logging makes debugging and log parsing easier.
Don't:
var db *sql.DB
func main() {
db = // ...
http.HandleFunc("/drop", DropHandler)
// ...
}
func DropHandler(w http.ResponseWriter, r *http.Request) {
db.Exec("DROP DATABASE prod")
}
Global variables make testing and readability hard and every method has access to them (even those, that don't need it).
Do:
func main() {
db := // ...
http.HandleFunc("/drop", DropHandler(db))
// ...
}
func DropHandler(db *sql.DB) http.HandleFunc {
return func (w http.ResponseWriter, r *http.Request) {
db.Exec("DROP DATABASE prod")
}
}
Use higher-order functions instead of global variables to inject dependencies accordingly.
Don't:
func TestAdd(t *testing.T) {
actual := 2 + 2
expected := 4
if (actual != expected) {
t.Errorf("Expected %d, but got %d", expected, actual)
}
}
Do:
import "github.com/stretchr/testify/assert" // for example
func TestAdd(t *testing.T) {
actual := 2 + 2
expected := 4
assert.Equal(t, expected, actual)
}
Using assert libraries makes your tests more readable, requires less code and provides consistent error output.
Don't:
func TestAdd(t *testing.T) {
assert.Equal(t, 1+1, 2)
assert.Equal(t, 1+-1, 0)
assert.Equal(t, 1, 0, 1)
assert.Equal(t, 0, 0, 0)
}
The above approach looks simpler, but it's much harder to find a failing case, especially when having hundreds of cases.
Do:
func TestAdd(t *testing.T) {
cases := []struct {
A, B, Expected int
}{
{1, 1, 2},
{1, -1, 0},
{1, 0, 1},
{0, 0, 0},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%d + %d", tc.A, tc.B), func(t *testing.T) {
assert.Equal(t, t.Expected, tc.A+tc.B)
})
}
}
Using table driven tests in combination with subtests gives you direct insight about which case is failing and which cases are tested. – Mitchell Hashimoto at GopherCon 2017
Don't:
func TestRun(t *testing.T) {
mockConn := new(MockConn)
run(mockConn)
}
Do:
func TestRun(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
t.AssertNil(t, err)
var server net.Conn
go func() {
defer ln.Close()
server, err := ln.Accept()
t.AssertNil(t, err)
}()
client, err := net.Dial("tcp", ln.Addr().String())
t.AssertNil(err)
run(client)
}
Only use mocks if not otherwise possible, favor real implementations. – Mitchell Hashimoto at GopherCon 2017
Don't:
type myType struct {
id int
name string
irrelevant []byte
}
func TestSomething(t *testing.T) {
actual := &myType{/* ... */}
expected := &myType{/* ... */}
assert.True(t, reflect.DeepEqual(expected, actual))
}
Do:
type myType struct {
id int
name string
irrelevant []byte
}
func (m *myType) testString() string {
return fmt.Sprintf("%d.%s", m.id, m.name)
}
func TestSomething(t *testing.T) {
actual := &myType{/* ... */}
expected := &myType{/* ... */}
if actual.testString() != expected.testString() {
t.Errorf("Expected '%s', got '%s'", expected.testString(), actual.testString())
}
// or assert.Equal(t, actual.testString(), expected.testString())
}
Using testString()
for comparing structs helps on complex structs with many fields that are not relevant for the equality check.
This approach only makes sense for very big or tree-like structs.
– Mitchell Hashimoto at GopherCon 2017
Only test unexported funcs if you can't access a path via exported funcs. Since they are unexported, they are prone to change.
Use linters (e.g. gometalinter) to lint your projects before committing.
Only commit gofmt'd files, use -s
to simplify code.
Don't:
func init() {
someStruct.Load()
}
Side effects are only okay in special cases (e.g. parsing flags in a cmd). If you find no other way, rethink and refactor.
In computer programming, a function may be considered a pure function if both of the following statements about the function hold:
- The function always evaluates the same result value given the same argument value(s). The function result value cannot depend on any hidden information or state that may change while program execution proceeds or between different executions of the program, nor can it depend on any external input from I/O devices.
- Evaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices.
Don't:
func MarshalAndWrite(some *Thing) error {
b, err := json.Marshal(some)
if err != nil {
return err
}
return ioutil.WriteFile("some.thing", b, 0644)
}
Do:
// Marshal is a pure func (even though useless)
func Marshal(some *Thing) ([]bytes, error) {
return json.Marshal(some)
}
// ...
This is obviously not possible at all times, but trying to make every possible func pure makes code more understandable and improves debugging.
Don't:
type Server interface {
Serve() error
Some() int
Fields() float64
That() string
Are([]byte) error
Not() []string
Necessary() error
}
func debug(srv Server) {
fmt.Println(srv.String())
}
func run(srv Server) {
srv.Serve()
}
Do:
type Server interface {
Serve() error
}
func debug(v fmt.Stringer) {
fmt.Println(v.String())
}
func run(srv Server) {
srv.Serve()
}
Favour small interfaces and only expect the interfaces you need in your funcs.
Deleting or merging packages is far more easier than splitting big ones up. When unsure if a package can be split, do it.
Don't:
func main() {
for {
time.Sleep(1 * time.Second)
ioutil.WriteFile("foo", []byte("bar"), 0644)
}
}
Do:
func main() {
logger := // ...
sc := make(chan os.Signal)
done := make(chan bool)
go func() {
for {
select {
case s := <-sc:
logger.Info("Received signal, stopping application",
zap.String("signal", s.String()))
break
default:
time.Sleep(1 * time.Second)
ioutil.WriteFile("foo", []byte("bar"), 0644)
}
}
done <- true
}()
signal.Notify(sc, os.Interrupt, os.Kill)
<-done // Wait for go-routine
}
Handling signals allows us to gracefully stop our server, close open files and connections and therefore prevent file corruption among other things.
Don't:
import (
"encoding/json"
"github.com/some/external/pkg"
"fmt"
"github.com/this-project/pkg/some-lib"
"os"
)
Do:
import (
"encoding/json"
"fmt"
"os"
"github.com/some/external/pkg"
"github.com/this-project/pkg/some-lib"
)
Dividing std, external and internal imports improves readability.
Don't:
func run() (n int, err error) {
// ...
return
}
Do:
func run() (n int, err error) {
// ...
return n, err
}
Named returns are good for documentation, unadorned returns are bad for readability and error-prone.
Don't:
package sub
Do:
package sub // import "github.com/my-package/pkg/sth/else/sub"
Adding the package comment adds context to the package and makes importing easy.
Don't:
func run(foo interface{}) {
// ...
}
Empty interfaces make code more complex and unclear, avoid them where you can.
Don't:
package main // import "github.com/me/my-project"
func someHelper() int {
// ...
}
func someOtherHelper() string {
// ...
}
func Handler(w http.ResponseWriter, r *http.Reqeust) {
// ...
}
func main() {
// ...
}
Do:
package main // import "github.com/me/my-project"
func main() {
// ...
}
func Handler(w http.ResponseWriter, r *http.Reqeust) {
// ...
}
func someHelper() int {
// ...
}
func someOtherHelper() string {
// ...
}
Putting main()
first makes reading the file a lot more easier. Only the init()
function should be above it.
If you're creating a cmd, consider moving libraries to internal/
to prevent import of unstable, changing packages.
Use clear names and try to avoid creating a helper.go
, utils.go
or even package.
To enable single-binary deployments, use tools to add templates and other static assets to your binary (e.g. github.com/jteeuwen/go-bindata).
type Config struct {
port int
timeout time.Duration
}
type ServerOpt func(*Config)
func WithPort(port int) ServerOpt {
return func(cfg *Config) {
cfg.port = port
}
}
func WithTimeout(timeout time.Duration) ServerOpt {
return func(cfg *Config) {
cfg.timeout = timeout
}
}
func startServer(opts ...ServerOpt) {
cfg := new(Config)
for _, fn := range opts {
fn(cfg)
}
// ...
}
func main() {
// ...
startServer(
WithPort(8080),
WithTimeout(1 * time.Second),
)
}