Skip to main content

Nerd Sniped About Go

·6 mins

I would like to dump some of my thoughts about what I’d rather do when writing some Go.

tl;dr:

Directory Structure #

This problem is already settled at golang-standards/project-layout, so I wont add too much into this.

What I’d rather do, and I personally think this will benefit the project:

πŸ‘‰ Use as flat as possible directory structure. #

Overly nested directory structure will increase cognition complexity for the projects, so make it simple.

πŸ‘‰ Consider to put everything in a single package. #

Due to Go unable to resolve circular import between packages, consider to put everything on the same package instead, for example miekg/dns is actually doing this, if there are people working on the same codebase with different scope/domains at the same time, you may want to take some time to design the structure, also take a look at semantic structuring below.

πŸ‘‰ If you’re concerned about placing something between /internal or /pkg, then put all of them into /internal. #

There’s really no need to worry about either, but if we’d like to follow the convention and pondering whether this package should be put in /pkg or /internal, prefer /internal at first. Since everything at /internal will be private to the module by default.

Semantic Structure #

Think first about your application/service in term of components, each component may be represented as its own struct, it’s a good habit to always think about interface since the beginning, especially if the team practicing contract-first-development / contract-driven-development, but it’s not a strict requirement, you can just put the methods you need to the component, and go with interface when you started grouping the same functionality of several components.

There are several grouping methods (layering), but this is what I prefer:

E n d p o i n t s S e r v i c e s C o m p o n e n t R s e p o s i t o r i e s S t o r a g e

MVC is overly simplistic and teach you the wrong way of thinking about your application structure, so don’t use it.

I would think first about the storage and repositories, as this will represent the persistence layer for our service.

The whole application’s high level business logics will be implemented as services, and calling into both repositories and components in order to perform its tasks.

Endpoints (i.e. http paths / routes) will need an explicit separated declaration, better if we can generate these code automatically, Twirp or oapi-codegen has good examples on how to do this.

The only notes here is:

  • Repositories should abstractize application persistence from its actual storage, so as much as possible, there should be no storage/DB specific API leaking from repositories to services or components.
  • Components only needed if the application has really complicated business flow and we already separate the logic that goes into data persistence, and some other logics that has its own processing (components).

Interfaces #

Interfaces can be a bit hairy in Go, due to its structural-typing nature, there are a couple of things that I’d like to keep in mind when working with interfaces in Go.

  • Be cautious about nil when returning (and then accepting) struct pointer from a function. This is because a nil-valued pointer for a given struct type, can still be considered as a valid interface implementation, implemented by that struct type, e.g.

    package main
    
    import "fmt"
    
    type A struct{}
    
    func (*A) Write() {
            fmt.Println("This is A")
    }
    
    func returnA() *A {
            return nil // deliberately returns nil
    }
    
    func main() {
            var a *A = returnA()
            a.Write()
            if a == nil {
                    fmt.Println("a is nil, blimey!")
            }
    }
    
    // outputs:
    // This is A
    // a is nil, blimey!
    
  • Consider to have interface guard when working in multiple persons team setup, i.e.

    // Given a struct
    type Struct struct {}
    
    // and an interface
    type Interface interface {
            Yay()
    }
    
    // You can declare dummy variable to force
    // *Struct to implement Interface, this will emit compiler error
    // if *Struct has not implementing `Yay` function
    var _ Interface = (*Struct)(nil)
    

    Although it looks a bit silly and almost unnecessary to have if you’re working alone, depend on how you work between interface and its implementor, it can be helpful if you’re working with people in a team setup, as this will explicitly saying that certain type will need to implement certain interface, emulating nominal typing in Go. (Credits to a colleague showing this pattern to me)

Misc. #

πŸ‘‰ init function is not needed at most of the time. #

Avoid to use this as much as possible, the only acceptable conditions when it’s okay to use init is either:

  • When you’re not bothered to change the related package behavior at (unit) tests, or even better: when you’re not bothered to write test at all for the related package /s.
  • When the package is a singleton for the whole application’s lifetime.

πŸ‘‰ Channels is the easiest way to ensure synchronization, it’s also can be tricky. #

According to a study about concurrency bugs in Go, there are more blocking bugs caused by message passing (Go channels), compared to by message sharing (mutex etc).

Some notes:

  • Go channel is blocking by default, unless we aware and do intend it to be blocking, I would prefer to create a buffered channel with a queue value of 1, to prevent the unintended blocking.
  • In other cases this can be prevented by ensuring we already have the receiver channel listening in a separate goroutine.
  • Buffered channel can be tricky depending on your use case,
    • For a single worker/goroutine serving many requests scenario, buffered channel can act as a queue, and this may increase concurrency.
    • But if you’re designing multiple workers mapped to multiple channels to handle many requests, buffered channel may decrease your concurrency as messages that can be sent to unoccupied channel may end up getting queued.
  • For production load concurrency patterns, prefer to use off-the-shelf solution such as sourcegraph/conc rather than rolling up your own solution.

πŸ‘‰ Use more Context #

I just realized about this recently, that I wasn’t use enough context, especially in a process with erratic behavior, context can help to flow control sparsed business logic. Please ensure to use cancelable context, and check whether the given context is already canceled and abort accordingly.

And that’s about it #

I will update this post if I can think of some other notes, or if I found errors in the notes above, or if I changed my mind.