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 thehttp.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.
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 onc
, thisf
variable contains the function that prints5
. - When
doSomething
is called ond
, thef
variable contains the function that prints12
.
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.
Get my free newsletter every second week
Used by 500+ developers to boost their Go skills.
"I'll share tips, interesting links and new content. You'll also get a brief guide to time for developers for free."