Weave Engineer/Carson Anderson/Stop Using Package Variables

Written by Carson Anderson Created Mon, 09 May 2022 23:02:11 -0600
421 Words

Summary

This lighting talk was presented at the Utah Go Meetup and covered some of the major pitfalls and problems that come with using package variables in Go.

Key Takeaways

Package variables often look like this:

package main

// x is a package variable
var x = 1

func main() {
    println(x)
}

A simple example like this might seem innocuous. But it is easy to introduce subtle bugs. Because any function in the package can manipulate them. You end up with shared state. And shared state is dangerous!

A more complete example that illustrates how bugs can be introduced can be seen in the following code:

// START MAIN OMIT
var (
	url = ""
)

func main() {
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "You called "+r.URL.Path)
	}))
	defer s.Close()
	url = s.URL

	go startHealthProbes()

	doHello()
	time.Sleep(time.Second)
	doHello()
}

The code above does the following:

  • It starts a web server which prints the requested url path for all requests
  • It sets the package variable url to the generated url for the test server
  • It starts a background health probe process to request /health every half second
  • It requests /
  • It waits one second
  • It requests / again

The code for doHello is trivial:

func doHello() {
	r, _ := http.Get(url)
	io.Copy(os.Stdout, r.Body)
	fmt.Println()
}

The code for startHealthProbes seems simple enough as well.

func startHealthProbes() {
	for range time.Tick(time.Second / 2) {
		doHeathProbe()
	}
}

func doHeathProbe() {
	// the health probe needs a timeout (default is 0 which means no timeout)
	http.DefaultClient.Timeout = time.Second * 30

	// the health probe needs to use a different url
	url = url + "/health"

	r, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer r.Body.Close()

	// read until EOF
	io.Copy(io.Discard, r.Body)
}

However, If you run the code above then you will see that the first call to doHello prints correctly, but the second call prints an unexpected result for the second call to doHello:

$ go run main.go
You called /
You called /health

The reason is that url is a package variable! So when doHeathProbe changes it to add the /health to the end, it breaks it for the second call to doHello.

This example is simple enough and could be avoided with a slight change to doHealthProbe but hopefully it illustrates one way of how package variables can introduce subtle bugs.

The full example code for the above problem is here

Details

The full source code for the talk can be found at: