Skip to content

computablelabs/computable-go-style

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 

Repository files navigation

Use Go Like So

COGS - COmputable Golang Style

Introduction

The goal of this document is to create consistently styled Go code as followed by Computable.

These are general guidelines, for further in-depth resources please see the following:

  1. Effective Go
  2. Common Go Mistakes

Tooling (optional)

Vscode users may get annoying linting issues] via the default linter. An improvement over the default linter is using GolangCI-Lint

Can be installed with the following command: go get -u github.com/go-delve/delve/cmd/dlv

Please follow the repo instructions to setup correctly.

Project Structure

For further detailed resources see the following:

  1. How to structure your Go apps by Kat Zien (video)
  2. Best practices for Industrial Programming by Peter Bourgon (video)
  3. Golang project layout

/cmd

  • Main application for this project
  • Don't put a lot of code in the application directory. If the code can be imported and used in other projects, it should live in /pkg.
  • It is common to have small main() functions. e.g.
func main() {
  if err := run(); err != nil {
    fmt.Printf(os.Stderr, "%v", err)
    os.Exit(1)
  }
}

/pkg

  • Code that's okay to be used by external applications.
  • Specifically we will be grouping packages by context as related by Domain Driven Design.

/internal

  • Private applications and library code.
  • Code you don't want others importing.
  • This layout pattern is enfored by the Go compiler.
  • You can have more than one internal directory at any level in the tree.

/vendor

  • Application dependencies

/configs

  • Configuration files

Domain Driven Design

The goal of DDD is to create a ubiquitous language allowing easy communication between cross-functional groups. It creates clarity by standardizing the use of words with pre-agreed meanings.

It also informs us how to structure our projects. This is known as "grouping by context." Our project structure will follow Domain Driven Design as best described by Kat Zien here.

Every repository with be accompanied with a D3.md featuring Ubiquitous Language that should be enforced by the package structure and actual code itself.

Prefer explicit idiomatic file names as opposed to default DDD naming. e.g.

Bad

/pkg/adding
--- service.go

Good

/pkg/adding
--- beer_adder.go

Style

Group Similar Declarations

BadGood
const a = 1
const b = 2



var a = 1
var b = 2



type Area float64
type Volume float64
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Do not group unrelated declarations.

BadGood
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  ENV_VAR = "MY_ENV"
)
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"

Import Group Ordering

There should be two import groups:

  • Standard library
  • Everything else
BadGood
import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)
import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Reduce Nesting

Code should reduce nesting where possible. This can be done by handling special cases/errors early.

BadGood
for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}
for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

And avoiding unnecessary else's.

BadGood
if ok {
  err := something()
  if err != nil {
    return errors.Wrap(err, "something")
  }
  // do things
} else {
  return errors.New("not ok")
}
if !ok {
  return errors.New("not ok")
}

err := something()
if err != nil {
  return errors.Wrap(err, "something")
}
// do things

Don't Panic

If errors occur, the function must return an error and allow the caller to decide how to handle.

BadGood
func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}
func foo(bar string) error {
  if len(bar) == 0 {
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}

Errors strings

Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation. Errors are usually usually printed following other context.

E.g. use fmt.Errorf("something bad") not fmt.Errorf("Something bad"), so that log.Printf("Reading %s: %v", filename, err) formats without a spurious capital letter mid-message.

Use field names to initialize Structs

Explicit should be favored over implicit.

BadGood
k := User{"John", "Doe", true}
k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Reduce scope of variables.

Reduce variable scope where possible. Do not reduce scope if it conflicts with reducing nesting.

BadGood
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}
if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}

Use Raw String Literals to Avoid Escaping

Go supports raw string literals, which can span multiple lines and include quotes. Use these to avoid hand-escaped strings which are much harder to read.

BadGood
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

Avoid using new

sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

Declaring empty slices

BadGood
t := []string{}
var t []string

Prefer minimal interfaces

Prefer using only as narrow an interface as needed.

BadGood
type File interface {
  io.Closer
  io.Reader
  io.ReaderAt
  io.Seeker
}

func ReadIn(f File) {
  b := []byte{}
  n, err := f.Read(b)
}
func ReadIn(r Reader) {
  b := []byte{}
  n, err := r.Read(b)
}

We needn't create an entire File to use ReadIn() when we're only interested in using the Reader.

Defer Close After Confirmed Open

BadGood
func getConfig() (*config, error) {
  var cfg config
  configFile, openErr := os.Open(SMART_CONTRACT_CONFIG_PATH)

  defer configFile.Close()

  if openErr != nil {
    return &cfg, openErr
  }
  // ...
}
func getConfig() (*config, error) {
  var cfg config
  configFile, openErr := os.Open(SMART_CONTRACT_CONFIG_PATH)

  if openErr != nil {
    return &cfg, openErr
  }
  defer configFile.Close()
  // ...
}

Minimal Method Signatures

Method signatures should be as minimal as possible.

When a single letter isn't available, use the shortest sensible two letter variable name.

BadGood
func AddListingHandlers(router Routable, list list.Listor, client projectorhttp.HttpClient, loggingChannel chan log.Message) Routable {
  // ...
}
func AddListingHandlers(r Routable, l list.Listor, c projectorhttp.HttpClient, lc chan log.Message) Routable {
  // ...
}

Constants should be SCREAMING_SNAKE_CASE

Our constants are in SCREAMING_SNAKE_CASE, in lieu of the commonly recommended camelCase. This was done for readability. They should never be imported.

BadGood
const userId = "userId"
const slug = "slug"
const xCorrelationId = "X-Correlation-ID"
const USER_ID = "userId"
const SLUG = "slug"
const X_CORRELATION_ID = "X-Correlation-ID"

Hexagonal Architecture

  • Our architecture is separated into Left, Center, Right.

  • Left

    • Application code
    • HTTP and REST for our API
    • Is free to import center
    • Must implement interfaces dictated by center
    • Drives the center
      • By instantiating aggregates/entities
      • By calling methods on center domain object, -able's
  • Center

    • Where we define aggregates/entities
    • Where we define center domain objects, -able's
    • Where we define our infrastructure interfaces, -or's
      • -or's are interfaces that define the center's ports
      • Right creates an adapter by fulfilling interfaces defined by these ports
      • Right and center meet where ports and adapters connect
    • Is importable by all
    • Drives right
    • Imports 3rd party libraries when context encapsulates 3rd party library
    • Is sectioned into Bounded Contexts
      • Contexts don't import each other
    • Does not import left or right
      • Prevents circular imports
      • Allows repository mocking
  • Right

    • Is free to import center
    • Can be
      • Infastructure
      • a Service (e.g. Stripe)
      • a Repository (e.g. Postgres, Dynamo)
    • Fulfills the -or interface as dictated by the center

Example

func registration(able register.Registerable, or register.Registeror) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {

		loggor := getChannelLoggor(r)
		correlationId(r)

		if err := decodeBody(r, able); err != nil {
			logging.LogError(loggor, err)
			respondHTTPErr(w, http.StatusBadRequest)
			return
		}
    
    # left drives center by calling method on the center domain object -able
		if err := able.Register(or, loggor); err != nil {
			logging.LogError(loggor, err)
			respondHTTPErr(w, http.StatusInternalServerError)
			return
		}

		respond(w, http.StatusOK, &able)
	}
}

About

Computable Golang Style Guide

Resources

Stars

0 stars

Watchers

3 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors