The Anonymous Anonymous Function

Photo by Kat J on Unsplash

The Anonymous Anonymous Function

A peculiar use for anonymous functions in Go

Today I came up against a unique challenge and landed upon an even more unique solution, so I felt it prudent to jot it down.

The Scenario

I don't often use the init() function in Go but today I did. I have a package called config in which I read some environment variables and store them in package-wide variables. These can then be obtained from other packages through exported functions in the config package. The bit where I read the environment variables and store them in package variables happens in the init() function. This way, any package which chooses to import the config package can happily obtain these config values without having to bother to initialise the config package first.

OK, that was boring. Let's rather show some code. Here's a very basic version of that config package. (lol, like looking at code isn't boring πŸ˜‚)

package config

import "os"

var cfg struct {
    apiKey string
    jwtSecret string
}

func init() {
    cfg.apiKey = os.Getenv("API_KEY")
    cfg.jwtSecret = os.Getenv("JWT_SECRET")
}

func ApiKey() string {
    return cfg.apiKey
}

func JwtSecret() string {
    return cfg.jwtSecret
}

The Problem

This works, or so I would like to believe. But we are Test Driven Developers, are we not? We already screwed up by writing any of this before we've even written the unit tests for it. But wait! I don't know how 😧

I don't think there is a way to write a unit test for init() and, while it's easy enough to write unit tests for ApiKey() and JwtSecret(), what do I actually put in those tests? Your (or at least my) immediate thought is to write something like:

func Test_ApiKey(t *testing.T) {
    os.Setenv("API_KEY", "12345")

    if got := ApiKey(); got != "12345" {
        t.Errorf("ApiKey() = %s, want %s", got, "12345")
    }
}

But see, that won't work. If you haven't spotted it already, by the time this Test_ApiKey(...) function is executed, the init() function has already done its thang and the value for cfg.apiKey which the ApiKey() function returns has already been set. No amount of os.Setenv(...) in my unit test is going to change any of that 😒

Multiple init() functions?

Did you know that Go allows you to have multiple init() functions in one package? In fact, you could even have multiple init() functions in a single file. So you'd be tempted to just add an init() function to your config_test.go file which would execute before the test functions, right?

Oof! So close, but still not good enough. The problem is that Go executes these init() functions in lexical order by the file name in which they occur. And, you guessed it, config.go comes before config_test.go. So while this new init() function will execute before your test functions, it still won't execute before the config.go file's init() function πŸ˜–

Enter stage left, the anonymous anonymous function

I'm giving it a silly name of course, but its proper description is no less of a mouthful. I'll get to that later but let me just show you the code first. This would be the config_test.go file then:

package config

import (
    "os"
    "testing"
)

var _ = func() (_ any) {
    os.Setenv("API_KEY", "12345")
    os.Setenv("JWT_SECRET", "AllTheHandsomePasswordsAreTaken")
    return
}()

func Test_ApiKey(t *testing.T) {
    if got := ApiKey(); got != "12345" {
        t.Errorf("ApiKey() = %s, want %s", got, "12345")
    }
}

func Test_JwtSecret(t *testing.T) {
    if got := JwtSecret(); got != "AllTheHandsomePasswordsAreTaken" {
        t.Errorf("JwtSecret() = %s, want %s", got, "AllTheHandsomePasswordsAreTaken")
    }
}

What on earth is that variable declaration up there? 😲 Let's look at it again, by itself:

var _ = func() (_ any) {
    os.Setenv("API_KEY", "12345")
    os.Setenv("JWT_SECRET", "AllTheHandsomePasswordsAreTaken")
    return
}()

So this is a niladic anonymous function that is defined and immediately called as part of a variable declaration. Let's break that down.

It's an anonymous function alright, we're familiar with that. But the trick here is, we want to be able to call it during variable declaration so that it can happen before the init() function is executed. As such, it has to return something, but we're not interested in anything it has to return; We're only interested in the side effect of the function running. So the variable we assign its value to is the blank identifier. To make this even weirder, the function makes use of, what we call, a "naked" return. That is, it has a named return value and a return statement without arguments. But the "named" return value is not really named either because it's also the blank identifier. Shall I call that a naked naked return?

But yes, this seems to be the solution to my problem. This function now runs before the init() function and I can seed the environment variables with values suitable to my tests.

Should I be doing this?

Probably not. I don't think this would be considered idiomatic Go, and Gophers Gophing Go are all about Gophing idiomatic Go. Moreover, I think this makes your code less readable which, to me, is an even greater sin.

In my case, I have no other choice (of course I do), and so, at the very least, I reckon I should be commenting the bleepers out of this section of code.

Another choice?

Do I really have no other choice? Of course I do and a valuable lesson I've learned in the past is that, if you find yourself having to do all kinds of odd coding gymnastics to get to your solution, you've probably overcomplicated it somewhere earlier in the process and you're now fixing problems that shouldn't exist. And you're probably busy creating further problems that will bite you later.

I think that's true here. I shouldn't be reading the environment variables in the init() function. There's no reason I couldn't just wait until another package calls config.ApiKey() before I actually read the API_KEY environment variable. And that is what I will be doing with this project I'm working on. I won't be making use of this anonymous anonymous function.

My method here is indeed creating further problems in that it limits me to having only a single test case for each environment variable. That's no bueno! By postponing until the config.ApiKey() function is called, I can set up a list of test cases and iterate over all of them like most unit tests are implemented.

But this post was really just a curious coder's look into some of Go's more subtle syntax. I'll just post it up here and leave it at that.

Β