8 min read

 

The Go language indisputably generates lot of discussions. Bjarne Stroustrup famously said:

There are only two kinds of languages: the ones people complain about and the ones nobody uses.

Many developers indeed share their usage retrospectives and the flaws they came to hate. No generics, no official tool for vendoring, built-in methods break the rules Go creators want us to endorse. The language ships with a bunch of principals and a strong philosophy.

Yet, The Go Gopher is making its way through companies. AWS is releasing its Go SDK, Hashicorp’s tools are written in Go, and so are serious databases like InfluxDB or Cockroach. The language doesn’t fit everywhere, but its concurrency model, its cross-platform binary format, or its lightning speed are powerful features. For the curious reader, Texlution digs deeper on Why Golang is doomed to succeed.

It is also intended to be simple. However, one should gain a clear understanding of the language’s conventions and data structures before producing efficient code. In this post, we will carefully setup a Go project to introduce a robust starting point for further development.

Tooling

Let’s kickoff the work with some standard Go project layout. New toys in town try to rethink the way they are organized, but I like to keep things simple as long as it just works. Assuming familiarity with the Go installation and GOPATH mess, we can focus on the code’s root directory.

➜ code tree -L 2
.
├── CONTRIBUTING.md
├── CHANGELOG.md
├── Gomfile
├── LICENCE
├── main.go
├── main_test.go
├── Makefile
├── shippable.yml
├── README.md
├── _bin
│   ├── gocov
│   ├── golint
│   ├── gom
│   └── gopm
└── _vendor
   ├── bin
   ├── pkg
   └── src

To begin with, README.md, LICENCE and CONTRIBUTING.md are usual important documents for any code expected to be shared or used. Especially with open source, we should care about and clearly state what the project does, how it works and how one can (and cannot) use it. Writing a Changelog is also a smart step in that direction.

Package manager

The package manager is certainly a huge matter of discussion among developers. The community was left to build upon the go get tool and many solutions arisen to bring deterministic builds to Go code. While most of them are good enough tools, Godep is the most widely used, but Gom is my personal favorite:

  • Simplicity with explicit declaration and tags
    # Gomfile
    
    gom 'github.com/gin-gonic/gin', :commit => '1a7ab6e4d5fdc72d6df30ef562102ae6e0d18518'
    gom 'github.com/ogier/pflag', :commit => '2e6f5f3f0c40ab9cb459742296f6a2aaab1fd5dc'
    
  • Dependency groups

    # Gomfile (continuation)
    
    group :test do
        # testing libraries
        gom 'github.com/franela/goblin', :commit => 'd65fe1fe6c54572d261d9a4758b6a18d054c0a2b'
        gom 'github.com/onsi/gomega', :commit => 'd6c945f9fdbf6cad99e85b0feff591caa268e0db'
        gom 'github.com/drewolson/testflight', :commit => '20e3ff4aa0f667e16847af315343faa39194274a'
    
        # testing tools
        gom 'golang.org/x/tools/cmd/cover'
        gom 'github.com/axw/gocov', :commit => '3b045e0eb61013ff134e6752184febc47d119f3a'
        gom 'github.com/mattn/goveralls', :commit => '263d30e59af990c5f3316aa3befde265d0d43070'
        gom 'github.com/golang/lint/golint', :commit => '22a5e1f457a119ccb8fdca5bf521fe41529ed005'
        gom 'golang.org/x/tools/cmd/vet'
    end
    
  • Self-contained project
    # install gom binary
    go get github.com/mattn/gom
    
    # ... write Gomfile ...
    
    # install production and development dependencies in `./_vendor`
    gom -test install
    

We just declared and bundled full requirements under its root directory. This approach plays nicely with trendy containers.

# we don't even need Go to be installed

# install tooling in ./_bin
mkdir _bin && export PATH=$PATH:$PWD/_bin
docker run --rm -it --volume $PWD/_bin:/go/bin golang go get -u -t github.com/mattn/gom

# asssuming the same Gomfile as above
docker run --rm -it 
    --volume $PWD/_bin:/go/bin 
    --volume $PWD:/app -w /app 
    golang gom -test install

An application can quickly rely on a significant number of external resources. Dependency managers like Gom offers a simple workflow to avoid breaking-change pitfalls – a widespread curse in our fast paced industry.

Helpers

The ambitious developer in love with productivity can complete its toolbox with powerful editor settings, an automatic fix, a Go repl, a debugger, and so on. Despite being young, the language comes with a growing set of tools helping developers to produce healthy codebase.

Code

With basic foundations in place, let’s develop a micro server powered by Gin, an impressive web framework I had great experience with. The code below highlights commonly best practices one can use as a starter.

// {{ Licence informations }}
// {{ build tags }}
// Package {{ pkg }} does ...
//
// More specifically it ...
package main

import (
   // built-in packages
   "log"
   "net/http"

   // third-party packages
   "github.com/gin-gonic/gin"
   flag "github.com/ogier/pflag"

   // project packages placeholder
)

// Options stores cli flags
type Options struct {
   // Addr is the server's binding address
   Addr string
}

// Hello greets incoming requests
// Because exported identifiers appear in godoc, they should be documented correctly
func Hello(c *gin.Context) {
   // follow HTTP REST good practices with an adequate http code and json-formatted response
   c.JSON(http.StatusOK, gin.H{ "hello": "world" })
}

// Handler maps endpoints with callbacks
func Handler() *gin.Engine {
   // gin default instance provides logging and crashing recovery middlewares
   router := gin.Default()

   router.GET("/greeting", Hello)

   return router
}

func main() {
   // parse command line flags
   opts := Options{}
   flag.StringVar(&opts.Addr, "addr", ":8000", "server address")
   flag.Parse()

   if err := Handler().Run(opts.Addr); err != nil {
       // exit with a message and a code status 1 on errors
       log.Fatalf("error running server: %vn", err)
   }
}

We’re going to take a closer look at two important parts this snippet is missing : error handling and interfaces’ benefits.

Errors

One tool we could have mentioned above is errcheck, which checks that you checked errors. While it sometimes produces cluttered code, Go error handling strategy enforces rigorous development :

  • When justified, use errors.New(“message”) to provide a helpful output.
  • If one needs custom arguments to produce a sophisticated message, use fmt.Errorf(“math: square root of negative number %g”, f)
  • For even more specific errors, let’s create new ones:
    type CustomError struct {
        arg int
        prob string
    }
    
    // Usage: return -1, &CustomError{arg, "can't work with it"}
    func (e *CustomError) Error() string {
        return fmt.Sprintf("%d - %s", e.arg, e.prob)
    }
    

Interfaces

Interfaces in Go unlock many patterns. In the gold age of components, we can leverage them for API composition and proper testing. The following example defines a Project structure with a Database attribute.

type Database interface {
    Write(string, string) error
    Read(string) (string, error)
}

type Project Structure {
    db Database
}

func main() {
    db := backend.MySQL()
    project := &Project{ db: db }
}

Project doesn’t care of the underlying implementation of the db object it receives, as long as this object implements Database interface (i.e. implements read and write signatures). Meaning, given a clear contract between components, one can switch Mysql and Postgre backends without modifying the parent object. Apart from this separation of concern, we can mock a Database and inject it to avoid heavy integration tests.

Hopefully this tiny, carefully written snippet should not hide too much horrors and we’re going to build it with confidence.

Build

We didn’t join a Test Driven Development style but let’s catch up with some unit tests. Go provides a full-featured testing package but we are going to level up the game thanks to a complementary combo. Goblin is a thin framework featuring Behavior-driven development close to the awesome Mocha for node.js. It also features an integration with Gomega, which brings us fluent assertions. Finally testflight takes care of managing the HTTP server for pseudo-integration tests.

// main_test.go
package main

import (
   "testing"

   . "github.com/franela/goblin"
   . "github.com/onsi/gomega"
   "github.com/drewolson/testflight"
)

func TestServer(t *testing.T) {
   g := Goblin(t)

   //special hook for gomega
   RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) })

   g.Describe("ping handler", func() {

       g.It("should return ok status", func() {
           testflight.WithServer(Handler(), func( r*testflight.Requester) {
               res := r.Get("/greeting")
               Expect(res.StatusCode).To(Equal(200))
           })
       })

   })
}

This combination allows readable tests to produce readable output. Given the crowd of developers who scan tests to understand new code, we added an interesting value to the project.

It would certainly attract even more kudos with a green test-suite. The following pipeline of commands try to validate a clean, bug-free, code smell-free, future-proof and coffee-maker code.

# lint the whole project package
golint ./...
# run tests and produce a cover report
gom test -covermode=count -coverprofile=c.out
# make this report human-readable
gocov convert c.out | gocov report
# push the reslut to https://coveralls.io/
goveralls -coverprofile=c.out -repotoken=$TOKEN

Conclusion

Countless posts conclude this way, but I’m excited to state that we merely scratched the surface of proper Go coding. The language exposes flexible primitives and unique characteristics one will learn the hard way one experimentation after another. Being able to trade a single binary against a package repository address is such an example, like JavaScript support.

This article introduced methods to kick-start Go projects, manage dependencies, organize code, offered guidelines and testing suite. Tweak this opinionated guide to your personal taste, and remember to write simple, testable code.

About the author

Xavier Bruhiere is the CEO of Hive Tech. He contributes to many community projects, including Occulus Rift, Myo, Docker and Leap Motion. In his spare time he enjoys playing tennis, the violin and the guitar. You can reach him at @XavierBruhiere.


Subscribe to the weekly Packt Hub newsletter. We'll send you the results of our AI Now Survey, featuring data and insights from across the tech landscape.

* indicates required

LEAVE A REPLY

Please enter your comment!
Please enter your name here