Skip to content

Idiomatic Go (in Skycoin)

gz-c edited this page Mar 9, 2019 · 10 revisions

Formatting

All code should be formatted with goimports. You can configure your text editor to do this automatically on save. Additionally, the project should have a Makefile command called make format which formats all of the source with goimports.

All files should end with a newline character. There should be no trailing whitespace on any line. This applies to all files, not just .go files, in a repository.

Testing

Use the testify require or testify assert package for assertions.

Mocking can be done with testify mock. Use mockery to autogenerate mocks.

Write table-driven tests when appropriate; most tests can be written this way.

Dependencies

Avoid the compulsive use of external dependencies and frameworks. Use them judiciously.

Code reuse

The DRY principle is not as strict in Go as it is in e.g. Python. Go does not always lend itself to DRY. Sometimes it is okay to copy code, don't overdesign.

Directory structure

Follow the Go conventions for directory structure. This means:

  • Commands go in /cmd/.
  • Library source code, if not at the top-level, goes in /pkg/.
  • Vendored dependencies go in /vendor/.
  • Test data files go into a folder named testdata/ in the relevant package.

Note: github.com/skycoin/skycoin uses src/ instead of pkg/ for historical reasons (the decision to use src/ was made before the Go community standardized on pkg/).

Filenames

Filenames should be lowercase only. Use underscores for multiword filenames.

Variable naming

  • Camel case is always used. Do not use underscores or all caps
  • Unexported names are lowerCamelCase
  • Exported names are UpperCamelCase
  • WRONG ALLCAPS WRONG
  • WRONG underscore_method WRONG
  • Variable names should be meaningful when possible

Use struct field names when declaring a struct object instance

Given this struct:

type Foo struct {
    Bar int
    Baz string
}

Use the struct's field names when instantiating it, and put them each on their own line:

var foo = Foo{
    Bar: 1,
    Baz: "cat",
}

Do not omit the names or put them all on the same line:

var foo = Foo{1, "cat"} // wrong
var foo2 = Foo{Bar: 1, Baz: "cat"} // wrong

If there is only one field name, sometimes it is ok to put it on the same line as the opening Foo{.

Returning errors

Return errors by checking err != nil:

x, err := foo()
if err != nil { 
    return err
}

or

if err := foo(); err != nil {
    return err
}

Error values and types

Callers of a method may want to distinguish certain errors from others. There are two ways to do this.

An instance of an error can be created at package scope during init. This instance of an error can be returned and compared against by callers.

Example:

package foo

var ErrSomethingWrong = errors.New("Something wrong")

func DoSomethingWrong() error {
    return ErrSomethingWrong
}
package main

import foo

func main() {
    err := foo.DoSomethingWrong()
    switch err {
        case foo.ErrSomethingWrong:
            fmt.Println("DoSomethingWrong returned ErrSomethingWrong")
        default:
            fmt.Println("DoSomethingWrong returned some other error")
    }
}

The other way is to use typed errors, which can allow the caller to check if an entire class of error had been received. The type can be an interface or a struct. An example of an interface type error in the golang stdlib is net.Error. An example of a struct type error in the golang stdlib is net.AddrError.

package foo

type Error struct {
    Val int
}

func DoSomethingWrong() error {
    return Error{
        Val: 1,
    }
}
package main

import foo

func main() {
    err := foo.DoSomethingWrong()
    switch err.(type) {
        case foo.Error:
            fmt.Println("This is a foo.Error")
        default:
            fmt.Println("This is not a foo.Error")
    }
}

Don't use named return values unless necessary

Named return values make the code more difficult to read and should only be used when necessary, such as when using a defer to catch an error on function exit. Sometimes it can be used when there are non-error return values and it is not clear what the meaning of each return value is.

For example do not write a function like this:

func foo(i int) (bar int, err error) {
    bar = i + 2
    return
}

Instead, write it like this:

func foo(i int) (int, error) {
    return i + 2, nil
}

It is ok to use it if an error in a defer call needs to be returned to the caller:

// CopyFile copy file
func CopyFile(dst string, src io.Reader) (n int64, err error) {
	// check the existence of dst file.
	if _, err := os.Stat(dst); err == nil {
		return 0, nil
	}
	err = nil

	out, err := os.Create(dst)
	if err != nil {
		return 0, err
	}
	defer func() {
		cerr := out.Close()
		if err == nil {
			err = cerr
		}
	}()

	n, err = io.Copy(out, src)
	return
}

It is also ok to use if there are a lot of return values and they would be ambiguous without names:

func segmentSampling(...) (start int, end int, step int) {
    ...
}

Do not use dot imports

Dot (.) imports pollute the namespace of the current package with another's. Don't do it. Avoid renaming the package import name too, unless necessary.

Clone this wiki locally