DEV Community

Cover image for Golang Patterns - Part 2
Nicola Apicella
Nicola Apicella

Posted on • Edited on

Golang Patterns - Part 2

Hi everyone! This is the second part of the article about common go lang patterns.
You can find the first part here.

One liner if statement

if err := someOperation(); err != nil {
    // error handling here
}
Enter fullscreen mode Exit fullscreen mode

I personally do not use it that much, but it can save a couple of keystrokes if all you want to do is returning the error upstream.

if err := someOperation(); err != nil {
    return errors.Wrap(err, "Ops, unable to complete some operation")
}
Enter fullscreen mode Exit fullscreen mode

Walker

I have seen this one used pretty much in all the codebases I stumbled upon.
Most notably, it is used in the path/filepath package.

The need is similar to the one addressed by the iterator pattern:

  • decoupling the algorithm from the data structure implementation
  • separate the logic necessary to iterate over data from the one which acts on it
func WalkInWordGrams(walker func(wordgram *wordgrams.WordGram) error) error {
   // Iteration happens here
}

WalkInWordGrams(func(wordGram *wordgrams.WordGram) error {
        for _, stats := range wordGram.Stats() {
            // ... do something meaningful with stats
        }

        return nil
    })
Enter fullscreen mode Exit fullscreen mode

In the code above we call a function WalkInWordGrams which takes as parameter a lambda which process data.
The lambda func is called for each element in the data structure.
In case of an unrecoverable error, we can stop the iteration by returning it.

Using what we know from the previous article, we can improve our DSL by adding a type for the lamdbda function.

type WordgramWalker func(wordgram *wordgrams.WordGram) error

func WalkInWordGrams(walker WordgramWalker) error {
   // Iteration happens here
}

WalkInWordGrams(func(wordGram *wordgrams.WordGram) error {
        for _, stats := range wordGram.Stats() {
            // ... do something meaningful with stats
        }

        return nil
    })

Enter fullscreen mode Exit fullscreen mode

Test data

This is something pretty useful when writing tests.
Go build ignores directory named testdataand when it runs tests it sets current directory as package directory. This allows you to use relative path testdata directory as a place to load and store our data.
For example we can write an utility function like the following:

func LoadTestFile(name string, failureHandler func(message string)) []byte {
    path := filepath.Join("testdata", name)
    bytes, e := ioutil.ReadFile(path)
    if e != nil {
        failureHandler(e.Error())
    }

    return bytes
}
Enter fullscreen mode Exit fullscreen mode

The function reads the file from testdata and returns it to the caller.
In case it fails, it calls the failure handler.
We can use it like this:

import (
    "bytes"
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    "io/ioutil"
    "net/http"
    "path/filepath"
    "testing"
)

func Test(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Test suite")
}

var _ = Describe("Some test which requires a file", func() {
    It("does what is supposed to do", func() {
        image := LoadTestFile("amalfi-coast.jpg", Fail)
                ...
Enter fullscreen mode Exit fullscreen mode

Godoc Example

This is something pretty cool, which helps to improve docs for our code.
From the golang docs:

Godoc examples are snippets of Go code that are displayed as package documentation and that are verified by running them as tests. They can also be run by a user visiting the godoc web page for the package and clicking the associated "Run" button.

Writing one is really easy. We need to write a function which name starts with Example in a test file.

func ExampleChain() {
    endpoint, _ := chain(
        loadEndpointFromConfigFile,
        loadEndpointFromEnvVariables,
        loadEndpointFromDatabase,
    ).get()

    fmt.Println(endpoint)
    // Output: some-endpoint
}

func loadEndpointFromEnvVariables() (string, error) {
    return "", nil
}

func loadEndpointFromConfigFile() (string, error) {
    return "", nil
}

func loadEndpointFromDatabase() (string, error) {
    return "some-endpoint", nil
} 
Enter fullscreen mode Exit fullscreen mode

The cool part is that the example becomes a test.
The Output comment is used to verify that the output matches our expectations.
It does so, by capturing data written to standard output and then comparing the output against the example's "Output:" comment. The test passes if the test's output matches its output comment.

Conclusions

Thanks for reading!
If you think I am missing some interesting pattern, let me know!

Top comments (2)

Collapse
 
tcarrio profile image
Tom

I'm a major novice (and not in the better way of interpretation) in Go, but to clarify:

func WalkInWordGrams(walker WordgramWalker) error {
Enter fullscreen mode Exit fullscreen mode

in your first code block, you had not set up the WordgramWalker type, so you couldn't use this could you? You would have had to had the func defined as:

func WalkInWordGrams(walker func(wordgram *wordgrams.WordGram) error) error {
Enter fullscreen mode Exit fullscreen mode

Or something along these lines?

Really interesting read, I just wanted to make sure I understood :D

Collapse
 
napicella profile image
Nicola Apicella

You are right, nice catch.
The example would fail to compile, because WordgramWalker is not defined.
The Ctr+C && Ctr+V pattern failed me XD

Thanks for reading and letting me know about the mistake!
I have updated the example with the correction :)