Advice on writing idiomatic Golang

Issue

I am in the process of getting to grips with the Golang way of doing things. First some sample code:

package main

import (
    "log"
    "os"
)

func logIt(s string) {
    f, _ := os.OpenFile("errors.log", os.O_RDWR|os.O_CREATE|os.O_APPEND,
        0666)
    defer f.Close()

    log.SetOutput(f)
    log.Println(s)
}

type iAm func(string)

func a(iam string) { logIt(iam + " A") }

func b(iam string) { logIt(iam + " B") }

func c(iam string) { logIt(iam + " C") }

var funcs = map[string]iAm{"A": a, "B": b, "C": c}

func main() {
    funcs["A"]("Je suis")
    funcs["B"]("Ich bin")
    funcs["A"]("Yo soy")
    funcs["D"]("Soy Yo")
}

Explanations

  • I am channeling all my log out put to a file so I can monitor it later. Is this the right way to channel?
  • I want to identify the right function to call at run time based on user inputs. To that end I have packed the functions as a Golang map – In PHP I would have used an associative array. This works. However, is this an efficient way to do things.
  • Finally, you will note that I don’t actually have a D key in my map. That last funcs call causes Go to throw a wobbly. In another language I would have wrapped those calls in a try… block and avoided the problem. From what I have understood the Go philosophy is to check the validity of the key first and panic rather than trying to blindly use that key. Is that correct?

I am a Go beginner so I probably have baggage from the other languages I use. To my mind dealing with exceptional conditions in a pre-emptive way (check the key prior to using it) is neither smart nor efficient. Right?

Solution

Logging to file

I wouldn’t open and close the file each time I want to log something. At startup I would just open it once and set it as output, and before the program exists, close it. And I wouldn’t use a logIt() function: just log using the functions of the log package, so you can do formatted logging e.g. with log.Printf() etc.

Dynamic function choosing

A function map is completely OK, and does well performance-wise. If you need something faster, you can do a switch based on the function name, and call directly the target function in the case branches.

Checking the existence of the key

The values in the map are function values. The zero value of a function type is nil and you can’t call a nil function, so you have to check the value before proceeding to call it. Note that if you index a map with a non-existing key, the zero-value of the value type is returned which is nil in case of function type. So we can simply check if the value is nil. There is also another comma-ok idiom, e.g. fv, ok := funcs[name] where ok will be a boolean value telling if the key was found in the map.

You can do it in one place though, you don’t have to duplicate it in each call:

func call(name, iam string) {
    if fv := funcs[name]; fv != nil {
        fv(iam)
    }
}

Note:

If you would choose to use a switch, the default branch would handle the invalid function name (and here you would not need the function map of course):

func call(name, iam string) error {
    switch name {
    case "A":
        a(iam)
    case "B":
        b(iam)
    case "C":
        c(iam)
    default:
        return errors.New("Unknown function: " + name)
    }
    return nil
}

Error handling / reporting

In Go functions can have multiple return values, so in Go you propagate error by returning an error value, even if the function normally has other return value(s).

So the call() function should have an error return type to signal if the specified function cannot be found.

You may choose to return a new error value created by e.g. the errors.New() function (so it can be dynamic) or you may choose to create a global variable and have a fixed error value like:

var ErrInvalidFunc = errors.New("Invalid function!")

The pros of this solution is that callers of the call() function can compare the returned error value to the value of the ErrInvalidFunc global variable to know that this is the case and act accordingly, e.g.:

if err := call("foo", "bar"); err != nil {
    if err == ErrInvalidFunc {
        // "foo" is an invalid function name
    } else {
        // Some other error
    }
}

So the complete revised program:

(Slightly compacted to avoid vertical scroll bars.)

package main

import ("errors"; "log"; "os")

type iAm func(string)

func a(iam string) { log.Println(iam + " A") }
func b(iam string) { log.Println(iam + " B") }
func c(iam string) { log.Println(iam + " C") }

var funcs = map[string]iAm{"A": a, "B": b, "C": c}

func main() {
    f, err := os.OpenFile("errors.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        panic(err)
    }
    defer f.Close()
    log.SetOutput(f)

    call("A", "Je suis")
    call("B", "Ich bin")
    call("C", "Yo soy")
    call("D", "Soy Yo")
}

func call(name, iam string) error {
    if fv := funcs[name]; fv == nil {
        return errors.New("Unknown funcion: " + name)
    } else {
        fv(iam)
        return nil
    }
}

Answered By – icza

Answer Checked By – Mildred Charles (GoLangFix Admin)

Leave a Reply

Your email address will not be published.