Confused by http.HandlerFunc? This post makes it click

Avatar of the author Willem Schots
14 Apr, 2023 (Updated 9 Sep, 2023)
~10 min.
RSS

When trying to use functions as Go http handlers, you will run into http.HandlerFunc from the net/http package.

If you’re not yet comfortable with Go’s type system and/or interfaces it can be a surprisingly difficult thing to wrap your head around. Several things can trip you up:

  • There are two similarly named http.Handle(r)Func functions/types that do different things.
  • Examples using http.HandlerFunc make it look like a function call, which it is not.
  • The way http.HandlerFunc implements the http.Handler interface.

In this article we’ll go over each of these difficulties and by the end you’ll - hopefully - have a solid understanding of http.HandlerFunc and can confidently use functions as handlers.

Are you or your team struggling with aspects of Go?

Book a 30 minute check-in with me.

I can help with questions like:

  • How should we structure our Go web app?
  • Is this package a good choice for us?
  • How do I get my team up to speed quickly?
  • Can I pick your brain on {subject}?
Learn more

Looking forward to meeting you.

- Willem

Background: functions as values

Before we take a look at http.HandlerFunc, it’s important to have a basic understanding of functions as values.

You know that you can declare variables and assign values to them:

// declare variable a of type int (initialized to default value 0).
var a int

// declare variable b of type int8 (initialized to value 22).   
var b int8 = 22

// declare variable c of type string (initialized to value "Abba Zaba").
c := "Abba Zaba"

a = 45 // assign 45 to a
b = 0  // assign 0 to b
c = "" // assign the empty string to c

You also know that you need to assign the right type to a variable, assinging the wrong type will result in a compilation error.

For example, if you try to assign a string to an int:

var a int
a = "Frownland"

You get the following compilation error:

cannot use "Frownland" (untyped string constant) as int value in assignment

These examples used basic types, but declaration and assignment work exactly the same for function types:

// declare variable d of type `func()`
// (initialized to default value nil).
var d func()

// assign a function that prints to stdout to d.
d = func() {
  fmt.Println("Ella guru")
}

// declare variable e of type `func() error`
// (initialized to a function that always returns nil).
var e func() error = func() error {
    return nil
}

// declare variable f of type `func(x int) bool`
// (initialized to a function that returns true if x > 5).
f := func(x int) bool {
  return x > 5
}

// declare a variable g of type `func() string`.
// (initialized to default value nil).
var g func() string

In this example the variables d, e, f and g all are variables with function types. The values for these variables must be either nil or a function that has a signature matching the type.

Again, if you try to assign a value of the wrong type you get a compilation error.

For example, when assigning a value of type func(x int) int to a variable of type func():

d = func(x int) int {
  return x + 1
}

You get the compilation error:

cannot use func(x int) int {…} (value of type func(x int) int) as func() value in assignment

The big difference between function types and other types, is that variables containing functions can be called like regular functions.

So building on the previous example, d, e, and f can be called like:

d() // prints: "Ella Guru"
err := e()
result := f(10)

fmt.Println(err, result) // prints: <nil> true

However, trying to call g will result in a panic due to a nil pointer dereference, since it’s value is nil:

g()
panic: runtime error: invalid memory address or nil pointer dereference

Keep functions as values in mind when we look further at http.HandlerFunc.

A first look at http.HandlerFunc

What is http.HandlerFunc?

If you check the documentation, you’ll see that it is a type definition:

// inside net/http package

type HandlerFunc func(ResponseWriter, *Request)

What does that mean?

A type definition essentially means: “create a custom type that is a …”.

You probably know that you can define custom types that are basic types or structs:

// defines a custom type myInt that is an int.
type myInt int

// defines a custom type myStruct that is
// a `struct with one field of type int`.
type myStruct struct {
  x int
}

// declares variable a of type myInt
// (initialized to value 20)
var a myInt = 20

// declares a variable b of type myStruct
// (initialized to a struct with a `x`-field value of 20)
b := myStruct{
  x: 20,
}

But you can also define custom types based on function types:

// defines a custom type myIntFunction that is
// a function with signature `func(x int) int`.
type myIntFunction func(x int) int

// declares variable c of type myIntFunction
// (initialized to a function that doubles its input).
var c myIntFunction = func(x int) int {
  return x * 2
}

Which is exactly what is happening in the http.HandlerFunc type definition:

// inside net/http package

// defines a custom type HandlerFunc that is
// a function with signature `func(ResponseWriter, *Request)`.
type HandlerFunc func(ResponseWriter, *Request)

Defining custom types gives you the possibily to add custom methods to them. We’ll discuss this further when we look at the http.Handler.

Do not confuse http.HandlerFunc with http.HandleFunc

In the net/http package there is also a function called http.HandleFunc (note the missing ‘r’). Docs can be found here.

It registers a path and handler function on the default serve mux. In general the default serve mux should not be used in production, and hence this function should not be used either.

It’s safe to ignore this completely, but it’s important not to confuse the two, because they do entirely different things.

Not a function call

In typical examples and tutorials you find online, you might see http.HandlerFunc used something like this:

package main

import (
	"log"
	"net/http"
)

func main() {
  srv := http.Server{
    Addr: ":8080",
    Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      w.Write([]byte("Dropout Boogie"))
    }),
  }
  err := srv.ListenAndServe()
  if err != nil {
	  log.Fatal(err)
  }
}

This program:

  • Creates a new http server with a single handler.
  • The handler responds to each request with "Dropout Boogie".
  • Makes the server listen and serve on port 8080.
  • Logs an error if the serving fails for any reason.

You might want to try and run it locally.

Let’s zoom in on the use of http.HandlerFunc:

// ...
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Dropout Boogie"))
}),
// ...

Initially you might interpret this to mean that http.HandlerFunc is a function call. But as we saw earlier, http.HandlerFunc is a function type, not a function.

So what is happening here?

Let’s find out. What happens if you remove it?

// ...
Handler: func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Dropout Boogie"))
},
// ...

If you run the modified program, you will get a compilation error:

cannot use func(w http.ResponseWriter, r *http.Request) {…} (value of type func(w http.ResponseWriter, r *http.Request)) as type http.Handler in struct literal:
	func(w http.ResponseWriter, r *http.Request) does not implement http.Handler (missing ServeHTTP method)

This is the compiler complaining that the Handler we provided is not of the right type: The compiler expects the handler to be an implementation of http.Handler (to which we will return in the next section).

So removing http.HandlerFunc(...) makes the function we provide to Handler of the wrong type.

In other words: Adding http.HandlerFunc(...) makes the function we provide of the right type.

The http.HandlerFunc(...) is a type conversion.

Type conversions

Just like you can convert between compatible basic types:

var a int32 = 12
var b int64 = int64(a) // convert from int32 to int64

You can also convert between function types if they have the same signatures:

type myFunctionOne func(x int) int
type myFunctionTwo func(y int) int

var c myFunctionOne = func(x int) int {
	return x * 2
}

// convert from myFunctionOne to myFunctionTwo
var d myFunctionTwo = myFunctionTwo(c)

But why does the compiler no longer complain once we convert our function to the http.HandlerFunc type?

Because http.HandlerFunc is the type that implements http.Handler.

http.Handler

What is http.Handler?

If we look at the docs again, we can see that it is an interface:

// inside net/http package

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

In Go, any type that has a ServeHTTP(ResponseWriter, *Request) method implements this interface.

You will probably know that you can add methods to struct types:

type myStruct struct {
	x int
}

 // define doSomething method on type *myStruct.
func (m *myStruct) doSomething() {
	fmt.Println(m.x) // print the field value.
}

And then these methods can then be called like:

a := &myStruct{x: 5}
b := &myStruct{x: 12}
a.doSomething() // prints: 5
b.doSoemthing() // prints: 12

But you can actually add methods to any type you define, including function types.

type myFunction func() int

 // define doSomething method on type myFunction
func(f myFunction) doSomething() {
	// call f (a value of type myFunction).
	result := f()
	// print the result of the function call.
	fmt.Println(result)
}

Which can then be called:

var c myFunction = func() int {
	return 5
}

var d myFunction = func() int {
	return 12
}

c.doSomething() // prints: 5
d.doSomething() // prints: 12

This can be a bit mind-bending if this is the first time you’re seeing something like this.

The “trick” here is to realize that inside the doSomething method, the f variable contains a “function as a value”:

  • When doSomething is called on c, this f variable contains the function that prints 5.
  • When doSomething is called on d, the f variable contains the function that prints 12.

If necessary, go back to the section on “functions as values” and play around with some of the examples locally.

The implementation

So how does http.HandlerFunc actually implement http.Handler?

Well, it is similar to the doSomething method we just saw:

// inside net/http package

type HandlerFunc func(ResponseWriter, *Request)

 // define ServeHTTP method on type HandlerFunc.
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	 // call f (a value of type HandlerFunc)
	 // with the provided parameters w and r.
	f(w, r)
}

The difference is that the ServeHTTP method has parameters, and doSomething did not. However, the only thing that happens with these parameters is that they are passed to f.

If we go back to our original example f variable would contain the original handler (the one that replies with “Dropout Boogie”).

This also shows that this handler is not called directly, but via ServeHTTP on the http.HandlerFunc type instead, which shows up in stack traces or step through it with a debugger.

A quick way to check this, (building on the earlier example) is to make the handler panic:

// ...
Handler: func(w http.ResponseWriter, r *http.Request) {
	panic("Safe as Milk")
},
// ...

If you now hit this endpoint, you will see a stack trace:

2023/03/13 18:45:43 http: panic serving 127.0.0.1:59218: Safe as milk
goroutine 6 [running]:
net/http.(*conn).serve.func1()
	/usr/local/go/src/net/http/server.go:1850 +0xbf
panic({0x62aa20, 0x6e0470})
	/usr/local/go/src/runtime/panic.go:890 +0x262
main.main.func1({0x4a5ce0?, 0xc0000d8080?}, 0x7f73f04c1268?)
	/home/user/main.go:12 +0x27
net/http.HandlerFunc.ServeHTTP(0x0?, {0x6e3048?, 0xc0000f8000?}, 0x46388e?)
	/usr/local/go/src/net/http/server.go:2109 +0x2f
net/http.serverHandler.ServeHTTP({0xc00008ae40?}, {0x6e3048, 0xc0000f8000}, 0xc0000ee000)
	/usr/local/go/src/net/http/server.go:2947 +0x30c
net/http.(*conn).serve(0xc000000b40, {0x6e3420, 0xc00008ad50})
	/usr/local/go/src/net/http/server.go:1991 +0x607
created by net/http.(*Server).Serve
	/usr/local/go/src/net/http/server.go:3102 +0x4db

If you look carefully you can indeed see that net/http.HandlerFunc.ServeHTTP was called.

Finishing up

It’s no wonder http.HandlerFunc can be confusing, it takes quite a bit of effort to follow the indirect function calls and type conversions. Even more so if you’re just starting out with Go.

But I hope that after reading this post you’re feeling a bit more comfortable using http.HandlerFunc in your programs. It can be a handy tool when you want to define http handlers in a concise way, or pass them around as “functions as values”.

Feel free to reach out if you have questions and/or comments.

If you want to read more posts like this, sign up for my newsletter below and receive them in your inbox.

🎓

Subscribe to my Newsletter and Keep Learning.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Twitter/X, LinkedIn or Mastodon.

Thanks for reading!