Go’s net/http and crypto/tls packages make enabling HTTP/2 surprisingly straightforward, but most people miss the subtle interplay with TLS version negotiation.
Let’s see it in action. We’ll set up a simple HTTP/2 server and then make a request to it using curl.
First, the server:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go HTTP/2 server! You requested: %s\n", r.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
// For TLS, we need a certificate and key.
// For local testing, you can generate them with:
// openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj "/CN=localhost"
certFile := "server.crt"
keyFile := "server.key"
server := &http.Server{
Addr: ":8443",
TLSConfig: &tls.Config{
// This is the crucial part for HTTP/2 negotiation.
// We need to explicitly enable the HTTP/2 server.
NextProtos: []string{"h2", "http/1.1"},
},
Handler: http.DefaultServeMux,
}
log.Printf("Starting HTTP/2 server on %s", server.Addr)
err := server.ListenAndServeTLS(certFile, keyFile)
if err != nil {
log.Fatal("ListenAndServeTLS: ", err)
}
}
And here’s how you’d generate the self-signed certificate and key for local testing:
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj "/CN=localhost"
Now, let’s start the Go server:
go run main.go
And make a request from curl (which supports HTTP/2):
curl -v --http2 https://localhost:8443/test
You should see output indicating that the connection is using HTTP/2:
* Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN protocol: h2
* Server certificate:
* subject: CN=localhost
* start date: Nov 28 12:00:00 2023 GMT
* expire date: Nov 27 12:00:00 2024 GMT
* issuer: CN=localhost
* SSL certificate verify ok.
> GET /test HTTP/2.0
> Host: localhost:8443
> user-agent: curl/7.81.0
> accept: */*
>
* Connection state changed (HTTP/2 confirmed)
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< date: Tue, 28 Nov 2023 12:00:00 GMT
<
Hello from Go HTTP/2 server! You requested: /test
* Connection #0 to host localhost left intact
The key to making this work lies in the TLSConfig.NextProtos field. When a client (like curl) initiates a TLS connection, it negotiates the application protocol to use over that secure channel. The NextProtos field is a list of protocols your server supports, in order of preference. By including "h2" (the identifier for HTTP/2) before "http/1.1", you’re telling the client, "I prefer HTTP/2, but I can fall back to HTTP/1.1 if you don’t support it." Go’s net/http server automatically handles the HTTP/2 framing and multiplexing when h2 is successfully negotiated.
The standard library’s http.Server is designed to be protocol-agnostic at its core. It uses the http.Server.TLSConfig.NextProtos to determine which protocol to use after the TLS handshake is complete. If "h2" is negotiated, the net/http server internally switches to its HTTP/2 implementation. If only "http/1.1" is negotiated, it uses the standard HTTP/1.1 handler. This allows a single http.Server instance to serve both HTTP/1.1 and HTTP/2 clients concurrently over the same port, provided the client supports the negotiation. You don’t need a separate server process or configuration for HTTP/2; it’s an opt-in mechanism during the TLS handshake.
One aspect that trips people up is that HTTP/2 requires TLS (or a secure connection). While the HTTP/2 RFC allows for unencrypted HTTP/2 (h2c), Go’s standard library net/http server only implements HTTP/2 over TLS. This means you must use ListenAndServeTLS and configure NextProtos correctly, even if you’re just testing locally. Without a valid certificate and key, ListenAndServeTLS will fail, and you won’t even get to the protocol negotiation stage. The NextProtos field doesn’t magically enable HTTP/2; it signals your intent to use it if the client agrees, and that agreement happens during the TLS handshake.
The next hurdle is often understanding how to configure client-side HTTP/2. Most modern HTTP clients, like curl and Go’s own net/http client, will attempt HTTP/2 negotiation by default when connecting to a server that advertises support for it via NextProtos during the TLS handshake. If you’re building a Go client and want to ensure HTTP/2 is used, you often don’t need to do anything special beyond using http.Get or http.Post with an https:// URL. The http.Transport will handle the negotiation. If you need more control, you might look at http.Transport.TLSClientConfig.NextProtos.