Skip to content

Instantly share code, notes, and snippets.

@brad-jones
Created June 4, 2018 02:40
Show Gist options
  • Save brad-jones/b41dda61bee71b50c44f91895baeea9c to your computer and use it in GitHub Desktop.
Save brad-jones/b41dda61bee71b50c44f91895baeea9c to your computer and use it in GitHub Desktop.
Get GOing with golang

Get GOing with golang

After initially starting with golang, I found it very refreshing, it's super fast, has a great supportive toolset, fantatsic IDE integrations, super duper easy cross compilation and so on.

After some time though, especially after building some more complex apps and libraries, I started to feel like I was in a giant mess of go code, sure it worked but many of the princiapls that have been so foundational in other languages, like loose coupling, dependency injection, inversion of control, even simple things like package structure and namespacing seem to have gone missing.

So this is my attempt to define some best practises, that compliement https://golang.org/doc/effective_go.html

Rules

  • A package should always define a canonical import. see: https://golang.org/doc/go1.4#canonicalimports

  • A package should export only a single "class-like" object. see: What does a golang class look like

  • A "class-like" object should have the same name as the package.

  • A "class-like" object should be in a file with the same name as the package.

  • A package can have many sub packages, in conjuction with canonical imports, this effectively creates namespaces.

  • A package can export many functions.

  • All additional "types" (structs, interfaces & function types) that do not not form the definition of the packages "class-like" object should go into a file named types.go

  • Each function/method should be in it's own file. Many small related functions (such as getters and setters) may be grouped together in a single file.

  • Each file in a package should have a corresponding _test.go file that runs unit/integration tests for that function.

  • The tests should be in their own package of the same name suffixed with _test this is so you are testing the public interface as it would used by any clients.

  • All functions & "class-like" objects should practise dependency injection to ensure components are lossely coupled and tests can easily mock dependants. see: What does dependency injection look like in golang

What does a golang class look like

Ok I get it, go does not have a true class as such. But we can create "class-like" objects, if anything it reminds me of JavaScript's prototype inheritance and how we used to create "class-like" objects before ES6 came along.

./src/github.com/brad-jones/foo/foo.go

package foo // import "github.com/brad-jones/foo"

// Define a public interface
type Foo interface {
    Bar(a string) bool
}

// Define the private class struct
// This ensures clients must use the New constructor function as intended
type foo struct {

    // NOTE: You may still define public fields if desired
    // But getter and setter methods would be preferable as these can be added
    // to the interface and for the same reasons you would use getters and
    // setters in other languages.
    Baz string
}

// The constructor for the class like object.
// Our return type is the interface and not the struct.
// This implicitly says `foo` implements `Foo` and will throw
// compile time errors if `foo` does not implement `Foo`
func New(qux bool) Foo {
	return &foo{}
}

// Like other languages you may define many constructors
// This can be useful to provide a default set of
// dependencies, when practising DI.
func NewWithQuxSetToTrue() Foo {
    return New(true)
}

./src/github.com/brad-jones/foo/bar.go

package foo // import "github.com/brad-jones/foo"

func (this *foo) Bar(a string) bool {
    return a == this.Baz
}

What does dependency injection look like in golang

DI in go is really not that hard, the same core pricipals still apply but there are some gotchas.

Ok so lets start with a simple example:

package foo // import "github.com/brad-jones/foo"

import "github.com/brad-jones/baz"

type Foo interface {
    Bar(a string) bool
}

type foo struct {
    aService baz.Baz
}

func New(dependency baz.Baz) Foo {
    // neat feature of go, you can forgo the field name
    // and initlise a struct, just with a list of values,
    // but you have to provide all values
    return &foo{dependency}
}
package main

import (
    "github.com/brad-jones/foo"
    "github.com/brad-jones/baz"
)

func main() {
    b := baz.New()
    f := foo.New(b)
}

As you can see we have successfully injected Baz into Foo.

What happens when the dependant package does not provide an interface?

Alot of go code out in the wild does not provide interfaces for their own exported "class-like" objects. Why? Because of the way go's type system works.

It's actually similar to TypeScript, I believe the correct terminalogy is to say that go & typescript are Structually Typed where as say C# and Java are Nominally Typed.

I'll attempt to demonstrate this here:

type Foo interface {
	DoNothing()
}

type Bar interface {
	DoNothing()
}

type baz struct {
}

func NewBazFoo() Foo {
	return &baz{}
}

func NewBazBar() Bar {
	return &baz{}
}

func (this *baz) DoNothing() {}

Take note of how NewBazFoo and NewBazBar are both valid. So long as baz structually matches the interface of either Foo or Bar then all is well.

So to answer the original question, if a package does not provide an interface for it's self, then simply create your own interface that matches the dependant package. It does not even have to match 100%. Maybe you only use a single method from the dependant package, just create an interface with that method and depend on that interface.

What about functions?

Most go code tends to be more functional than object oriented but again DI is not hard. Just inject functions into functions.

func Foo(a string) bool {
    return a == "b"
}

func Bar(a string, foo func(a string) bool) bool
{
    return foo(a)
}

Bar("abc", Foo)

You can also do this to define what is effectily an interface for a function.

type Foo func(a string) bool

Once you do this though, you lose out on some IDE autocomplete functionality. At least with https://github.com/Microsoft/vscode-go

Maybe your class or function requires a collection of functions, that would be too many to list inline, then do something like:

type dependencies struct {
    foo func(a string) bool
    bar func(a int) bool
}

func Baz(a string, b int, deps *dependencies) bool
{
    return deps.foo(a) && deps.bar(b)
}

Baz("abc", &dependencies{
    foo: func(a string) bool { return false },
    bar: func(a int) bool { return false },
})

What does Inversion of Control look like in golang, do I need an IoC container?

Short answer, no you don't need an IoC container!

Assuming we have practised DI in all our components then we still want to practise Inversion of Control we just don't need a container to do it.

Your func main() method becomes your container.

  • If you need a singleton, you create a new instance of something, save it to a variable and inject it into other services as needed.

  • If you need a transient value (is re-created each time it is needed) then create a factory function and inject that into your services.

  • go is like javascript when it comes to variable scoping and functions / closures so you can get creative inside func main() by passing grandparent services into factories without necessarily having to share those grandparents with the intermediary services.

  • By not using a dynamic runtime container you will get compile time saftey, not to mention not having to pay the perfomance impact of reflection.

  • Also because go is structually typed it is very difficult / error prone to bind an interface to a concreation. As we have demonstrated 2 interfaces can match one another. Any binding can only be done with brittle reflected strings.

package main

func main() {
    aService := a.New()
    bService := b.New(aService)
    cServiceFactory := func(){ return c.New(aService) }
    dService := d.New(bService, cServiceFactory)
    dService.Execute()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment