Sometimes you have to do things over and over again to really internalize the concepts and theory. The same is true for programming languages. Take a Go function for example. When you are like me and program in different languages, its really important to ‘speak the language’. In this blog we’ll learn how to structure Go functions in an idiomatic way.
A simple function
Lets examine the following function that creates an S3 client for a specific region and returns a tuple of S3 client and an error. Go functions return errors early and return values late. Return an error early means returning an error when a problem arises. Return a value late means that at the end of the function a value should be returned. The contents of the tuple can vary depending on when the function returned. When an error occurs, the value will be nil, and the error has a value. When there is no problem, error is nil, and there is a value.
The Go style of creating functions means that errors should always be checked first. When the errors have been properly addressed, the only thing that remains is actually working with the value.
func CreateS3Client(region string) (*s3.S3, error) {
sess, err := session.NewSession()
if err != nil {
return nil, err
}
svc := s3.New(sess, aws.NewConfig().WithRegion(region))
return svc, nil
}
For example, the following snippet comes from io/ioutil
and shows the same style of coding a function.
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
var n int64 = bytes.MinRead
if fi, err := f.Stat(); err == nil {
if size := fi.Size() + bytes.MinRead; size > n {
n = size
}
}
return readAll(f, n)
}
Line of sight
The Go style of handling errors in an if-block means that the code is structured in such a way that error handling is indented from the value handling. The effect of the style is called line-of-sight and is shown in the image below. The green line shows that ‘value-handling’ is at the green line, and the red line is ‘error-handling’. When you look at Go codebases, you can easily scan the code by looking at the indentation.
Handling Errors
The main function is different. In main we still check for errors first, but instead of returning errors, we handle errors early. Each time we call a function that returns a value/error pair, we must handle the error. The result is the same style of handling errors indented and handing values at the left side of the function.
func main() {
svc, err := CreateS3Client("eu-west-1")
if err != nil {
log.Fatal(err)
}
buckets, err := ListBuckets(svc)
if err != nil {
log.Fatal(err)
}
RenderBuckets(buckets)
}
Conclusions
Writing idiomatic Go functions consists of returning errors early and returning values late. In the main function we handle errors whenever we receive a value/error pair. The Go style of specifying functions results in a line-of-sight where we can easily focus on where errors are handled, the indented blocks. The main flow of computing with values is always at the left side of the code block. Go code is very imperative by nature and that is a good thing as it results in understandable code. Time for some more katas!