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
-
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
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
}
DI in go
is really not that hard, the same core pricipals still apply but
there are some gotchas.
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.
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.
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 },
})
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 insidefunc 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()
}