Tools: How To Make an HTTP Server in Go (2026)

Tools: How To Make an HTTP Server in Go (2026)

Source: DigitalOcean

By Kristin Davidson, Rachel Lee and Manikandan Kurup Many developers spend at least some of their time creating servers to distribute content on the internet. The Hypertext Transfer Protocol (HTTP) serves much of this content, whether it’s a request for a cat image or a request to load the tutorial you’re reading now. The Go standard library provides built-in support for creating an HTTP server to serve your web content or making HTTP requests to those servers. In this tutorial, you will create an HTTP server using Go’s standard library and then expand your server to read data from the request’s query string, the body, and form data. You’ll also update your program to respond to requests with custom HTTP headers and status codes. Additionally, you’ll learn how to implement middleware for reusable request processing, build JSON API endpoints, and apply production-ready practices including timeouts, graceful shutdown, and HTTPS configuration. To follow this tutorial, you will need: In Go, most of the HTTP functionality is provided by the net/http package in the standard library, while the rest of the network communication is provided by the net package. The net/http package not only includes the ability to make HTTP requests, but also provides an HTTP server you can use to handle those requests. In this section, you will create a program that uses the http.ListenAndServe function to start an HTTP server that responds to the request paths / and /hello. Then, you will expand that program to run multiple HTTP servers in the same program. Before you write any code, though, you’ll need to get your program’s directory created. Many developers keep their projects in a directory to keep them organized. In this tutorial, you’ll use a directory named projects. First, make the projects directory and navigate to it: Next, make the directory for your project and navigate to that directory. In this case, use the directory httpserver: Now that you have your program’s directory created and you’re in the httpserver directory, you can start implementing your HTTP server. A Go HTTP server includes two major components: the server that listens for requests coming from HTTP clients and one or more request handlers that will respond to those requests. In this section, you’ll start by using the function http.HandleFunc to tell the server which function to call to handle a request to the server. Then, you’ll use the http.ListenAndServe function to start the server and tell it to listen for new HTTP requests and then serve them using the handler functions you set up. Now, inside the httpserver directory you created, use nano, or your favorite editor, to open the main.go file: In the main.go file, you will create two functions, getRoot and getHello, to act as your handler functions. Then, you’ll create a main function and use it to set up your request handlers with the http.HandleFunc function by passing it the / path for the getRoot handler function and the /hello path for the getHello handler function. Once you’ve set up your handlers, call the http.ListenAndServe function to start the server and listen for requests. Add the following code to the file to start your program and set up the handlers: In this first chunk of code, you set up the package for your Go program, import the required packages for your program, and create two functions: the getRoot function and the getHello function. Both of these functions have the same function signature, where they accept the same arguments: an http.ResponseWriter value and an *http.Request value. This function signature is used for HTTP handler functions and is defined as http.HandlerFunc. When a request is made to the server, it sets up these two values with information about the request being made and then calls the handler function with those values. In an http.HandlerFunc, the http.ResponseWriter value (named w in your handlers) is used to control the response information being written back to the client that made the request, such as the body of the response or the status code. Then, the *http.Request value (named r in your handlers) is used to get information about the request that came into the server, such as the body being sent in the case of a POST request or information about the client that made the request. For now, in both of your HTTP handlers, you use fmt.Printf to print when a request comes in for the handler function, then you use the http.ResponseWriter to send some text to the response body. The http.ResponseWriter is an io.Writer, which means you can use anything capable of writing to that interface to write to the response body. In this case, you’re using the io.WriteString function to write your response to the body. While http.HandlerFunc is a convenient way to use a plain function as a request handler, it’s worth understanding the http.Handler interface it’s built on. The http.Handler interface is defined as any type that implements a single method: Any struct that has a ServeHTTP method with this signature satisfies the interface and can be used anywhere Go’s HTTP package expects a handler, including as the second argument to http.ListenAndServe. This becomes useful when your handler needs to carry state, such as a database connection or a configuration value, that you don’t want to pass through a global variable: http.HandlerFunc, by contrast, is a shortcut. It is itself a type that implements http.Handler, but it lets you treat any function with the right signature as a handler without defining a new struct. Under the hood, its ServeHTTP method simply calls the function itself. So when you write: Go is converting your getRoot function into an http.HandlerFunc value and registering it as an http.Handler on the mux. The two approaches produce the same result; the distinction is one of convenience versus structure. For simple handlers, http.HandlerFunc keeps the code concise. For handlers that need to share state or be tested in isolation, implementing http.Handler directly on a struct is the cleaner choice. Now, continue creating your program by starting your main function: In the main function, you have two calls to the http.HandleFunc function. Each call to the function sets up a handler function for a specific request path in the default server multiplexer. The server multiplexer is an http.Handler that is able to look at a request path and call a given handler function associated with that path. So, in your program, you’re telling the default server multiplexer to call the getRoot function when someone requests the / path and the getHello function when someone requests the /hello path. Once the handlers are set up, you call the http.ListenAndServe function, which tells the global HTTP server to listen for incoming requests on a specific port with an optional http.Handler. In your program, you tell the server to listen on ":3333". By not specifying an IP address before the colon, the server will listen on every IP address associated with your computer, and it will listen on port 3333. A network port, such as 3333 here, is a way for one computer to have many programs communicating with each other at the same time. Each program uses its own port, so when a client connects to a specific port the computer knows which program to send it to. If you wanted to only allow connections to localhost, the hostname for IP address 127.0.0.1, you could instead say 127.0.0.1:3333. Your http.ListenAndServe function also passes a nil value for the http.Handler parameter. This tells the ListenAndServe function that you want to use the default server multiplexer and not the one you’ve set up. The ListenAndServe is a blocking call, which means your program won’t continue running until after ListenAndServe finishes running. However, ListenAndServe won’t finish running until your program finishes running or the HTTP server is told to shut down. Even though ListenAndServe is blocking and your program doesn’t include a way to shut down the server, it’s still important to include error handling because there are a few ways calling ListenAndServe can fail. So, add error handling to your ListenAndServe in the main function as shown: The first error you’re checking for, http.ErrServerClosed, is returned when the server is told to shut down or close. This is usually an expected error because you’ll be shutting down the server yourself, but it can also be used to show why the server stopped in the output. In the second error check, you check for any other error. If this happens, it will print the error to the screen and then exit the program with an error code of 1 using the os.Exit function. One error you may see while running your program is the address already in use error. This error can be returned when ListenAndServe is unable to listen on the address or port you’ve provided because another program is already using it. Sometimes this can happen if the port is commonly used and another program on your computer is using it, but it can also happen if you run multiple copies of your own program multiple times. If you see this error as you’re working on this tutorial, make sure you’ve stopped your program from a previous step before running it again. Note: If you see the address already in use error and you don’t have another copy of your program running, it could mean some other program is using it. If this happens, wherever you see 3333 mentioned in this tutorial, change it to another number above 1024 and below 65535, such as 3334, and try again. If you still see the error you may need to keep trying to find a port that isn’t being used. Once you find a port that works, use that going forward for all your commands in this tutorial. Now that your code is ready, save your main.go file and run your program using go run. Unlike other Go programs you may have written, this program won’t exit right away on its own. Once you run the program, continue to the next commands: Since your program is still running in your terminal, you will need to open a second terminal to interact with your server. When you see commands or output with the same color as the command below, it means to run it in this second terminal. In this second terminal, use the curl program to make an HTTP request to your HTTP server. curl is a utility commonly installed by default on many systems that can make requests to servers of various types. For this tutorial, you’ll be using it to make HTTP requests. Your server is listening for connections on your computer’s port 3333, so you’ll want to make your request to localhost on that same port: The output will look like this: In the output you’ll see the This is my website! response from the getRoot function, because you accessed the / path on your HTTP server. Now, in that same terminal, make a request to the same host and port, but add the /hello path to the end of your curl command: Your output will look like this: This time you’ll see the Hello, HTTP! response from the getHello function. If you refer back to the terminal you have your HTTP server function running in, you now have two lines of output from your server. One for the / request and another for the /hello request: Since the server will continue running until the program finishes running, you’ll need to stop it yourself. To do this, press CONTROL+C to send your program the interrupt signal to stop it. In this section, you created an HTTP server program, but it’s using the default server multiplexer and default HTTP server. Using default, or global, values can lead to bugs that are hard to duplicate because multiple parts of your program could be updating them at different and varying times. If this leads to an incorrect state, it can be hard to track down the bug because it might only exist if certain functions are called in a particular order. So, to avoid this problem, you’ll update your server to use a server multiplexer you create yourself in the next section. When you started your HTTP server in the last section, you passed the ListenAndServe function a nil value for the http.Handler parameter because you were using the default server multiplexer. Because http.Handler is an interface, it’s possible to create your own struct that implements the interface. But sometimes, you only need a basic http.Handler that calls a single function for a specific request path, like the default server multiplexer. In this section, you will update your program to use http.ServeMux, a server multiplexer and http.Handler implementation provided by the net/http package, which you can use for these cases. The http.ServeMux struct can be configured the same as the default server multiplexer, so there aren’t many updates you need to make to your program to start using your own instead of a global default. To update your program to use an http.ServeMux, open your main.go file again and update your program to use your own http.ServeMux: In this update, you created a new http.ServeMux using the http.NewServeMux constructor and assigned it to the mux variable. After that, you only needed to update the http.HandleFunc calls to use the mux variable instead of calling the http package. Finally, you updated the call to http.ListenAndServe to provide it the http.Handler you created (mux) instead of a nil value. Now you can run your program again using go run: Your program will continue running as it did last time, so you’ll need to run commands to interact with the server in another terminal. First, use curl to request the / path again: The output will look like the following: You’ll see this output is the same as before. Next, run the same command from before for the /hello path: The output will look like this: The output for this path is the same as before as well. Finally, if you refer back to the original terminal you’ll see the output for both the / and /hello requests as before: The update you made to the program is functionally the same, but this time you’re using your own http.Handler instead of the default one. Finally, press CONTROL+C again to exit your server program. In the above section, you updated your program to use an http.ServeMux instead of the default server multiplexer. The ServeMux type provided by the net/http package is a simple and effective way to route incoming HTTP requests to handler functions based on the request path. The ServeMux works by matching the URL path of an incoming request to a registered pattern and then calling the corresponding handler. Because it is part of the Go standard library, it integrates seamlessly with the http.Server type and requires no additional dependencies. This makes it a strong choice for small applications and for learning how HTTP routing works in Go. However, ServeMux is intentionally minimal in its design. It supports: For many applications, this level of functionality is sufficient. As your application grows, though, you may encounter routing requirements that are more complex. One common example is the need for dynamic or parameterized routes. Consider a URL such as: In this case, the value 42 represents a user ID. With ServeMux, handling this type of route typically requires manually parsing the URL path inside the handler. While this approach works, it can become difficult to manage as the number of routes increases. To address these limitations, many developers turn to third-party routers that provide more advanced routing capabilities. Popular options in the Go ecosystem include: These routers extend the functionality of ServeMux and commonly support features such as: For instance, a router may allow you to define a route like /users/{id} and automatically extract the value of id from the request URL. This reduces the need for manual parsing and keeps handler logic focused on processing the request. Another advantage of many third-party routers is their support for middleware. While middleware can be implemented using the http.Handler interface, as shown earlier in this tutorial, routers often provide convenient ways to apply middleware across groups of routes or the entire application. Despite these additional features, most third-party routers are designed to integrate with the standard net/http package rather than replace it. They typically implement the http.Handler interface, which means they can be used with http.ListenAndServe or an http.Server in the same way as a ServeMux. This compatibility allows you to adopt a router incrementally. In many cases, switching from ServeMux to a third-party router only requires replacing the multiplexer while leaving the rest of your server configuration unchanged. For learning purposes and simple applications, http.ServeMux remains an excellent starting point because it introduces routing concepts without additional abstraction. As your application grows and your routing needs become more complex, you can evaluate whether a third-party router would simplify your implementation and improve maintainability. By understanding both approaches, you can choose the routing solution that best fits your application while continuing to build on the same core concepts provided by Go’s HTTP server. In addition to using your own http.Handler, the Go net/http package also allows you to use an HTTP server other than the default one. Sometimes you may want to customize how the server runs, or you may want to run multiple HTTP servers in the same program at once. For example, you may have a public website and a private admin website you want to run from the same program. Since you can only have one default HTTP server, you wouldn’t be able to do this with the default one. In this section, you will update your program to use two http.Server values provided by the net/http package for cases like these — when you want more control over the server or need multiple servers at the same time. In your main.go file, you will set up multiple HTTP servers using http.Server. You’ll also update your handler functions to access the context.Context for the incoming *http.Request. This will allow you to set which server the request is coming from in a context.Context variable, so you can print the server in the handler function’s output. Open your main.go file again and update it as shown: In the code update above, you updated the import statement to include the packages needed for the update. Then, you created a const string value called keyServerAddr to act as the key for the HTTP server’s address value in the http.Request context. Lastly, you updated both your getRoot and getHello functions to access the http.Request’s context.Context value. Once you have the value, you include the HTTP server’s address in the fmt.Printf output so you can see which of the two servers handled the HTTP request. Now, start updating your main function by adding the first of your two http.Server values: In the updated code, the first thing you did is create a new context.Context value with an available function, cancelCtx, to cancel the context. Then, you define your serverOne http.Server value. This value is very similar to the HTTP server you’ve already been using, but instead of passing the address and the handler to the http.ListenAndServe function, you set them as the Addr and Handler values of the http.Server. One other change is adding a BaseContext function. BaseContext is a way to change parts of the context.Context that handler functions receive when they call the Context method of *http.Request. In the case of your program, you’re adding the address the server is listening on (l.Addr().String()) to the context with the key serverAddr, which will then be printed to the handler function’s output. Next, define your second server, serverTwo: This server is defined the same way as the first server, except instead of :3333 for the Addr field, you set it to :4444. This way, one server will be listening for connections on port 3333 and the second server will listen on port 4444. Now, update your program to start the first server, serverOne, in a goroutine: Inside the goroutine, you start the server with ListenAndServe, the same as you have before, but this time you don’t need to provide parameters to the function like you did with http.ListenAndServe because the http.Server values have already been configured. Then, you do the same error handling as before. At the end of the function, you call cancelCtx to cancel the context being provided to the HTTP handlers and both server BaseContext functions. This way, if the server ends for some reason, the context will end as well. Finally, update your program to start the second server in a goroutine as well: This goroutine is functionally the same as the first one, it just starts serverTwo instead of serverOne. This update also includes the end of the main function where you read from the ctx.Done channel before returning from the main function. This ensures that your program will stay running until either of the server goroutines ends and cancelCtx is called. Once the context is over, your program will exit. Save and close the file when you’re done. Run your server using the go run command: Your program will continue running again, so in your second terminal run the curl commands to request the / path and the /hello path from the server listening on 3333, the same as previous requests: The output will look like this: In the output you’ll see the same responses you saw before. Now, run those same commands again, but this time use port 4444, the one that corresponds to serverTwo in your program: The output will look like the following: You’ll see the same output for these requests as you did for the requests on port 3333 being served by serverOne. Finally, look back at the original terminal where your server is running: The output looks similar to what you saw before, but this time it shows the server that responded to the request. The first two requests show they came from the server listening on port 3333 (serverOne), and the second two requests came from the server listening on port 4444 (serverTwo). These are the values retrieved from the BaseContext’s serverAddr value. Your output may also be slightly different than the output above depending on whether your computer is set up to use IPv6 or not. If it is, you’ll see the same output as above. If not, you’ll see 0.0.0.0 instead of [::]. The reason for this is that your computer will communicate with itself over IPv6 if configured, and [::] is the IPv6 notation for 0.0.0.0. Once you’re done, use CONTROL+C again to stop the server. In this section, you created a new HTTP server program using http.HandleFunc and http.ListenAndServe to run and configure the default server. Then, you updated it to use an http.ServeMux for the http.Handler instead of the default server multiplexer. Finally, you updated your program to use http.Server to run multiple HTTP servers in the same program. While you have an HTTP server running now, it’s not very interactive. You can add new paths that it responds to, but there’s not really a way for users to interact with it past that. The HTTP protocol includes a number of ways users can interact with an HTTP server beyond paths. In the next section, you’ll update your program to support the first of them: query string values. As your HTTP server grows, you may notice that several handler functions begin to share common behavior. For instance, you might want to log details about every request, add headers to each response, or perform validation before a handler runs. If you add this logic directly into each handler function, your code can quickly become repetitive and harder to maintain. To address this, Go applications commonly use a pattern called middleware. Middleware allows you to wrap an existing handler with additional functionality so that the shared logic is executed automatically for every request. This pattern works well in Go because all HTTP handlers follow the same http.Handler interface introduced earlier in this tutorial. In Go, middleware is typically written as a function that accepts an http.Handler and returns another http.Handler. This returned handler can run code before and after calling the original handler, allowing you to insert additional behavior into the request lifecycle. To see how this works, open your main.go file and add the following function: This function wraps another handler, referred to as next, and returns a new handler. When a request is received, the middleware first prints a message indicating that the request has started. It then calls next.ServeHTTP(w, r) to pass control to the original handler. After the handler finishes processing the request, the middleware prints another message indicating that the request has completed. The call to next.ServeHTTP is what allows the request to continue through the handler chain. If this call were omitted, the request would never reach your handler function, and no response would be generated. Once the middleware function has been defined, you can apply it to your server by wrapping your existing handler or multiplexer. If you are using an http.ServeMux, update your main function as shown below: In this example, the mux variable still handles routing requests to the appropriate handler functions. However, instead of passing mux directly to http.ListenAndServe, it is wrapped using loggingMiddleware. This means every request will pass through the middleware before reaching the handler functions you defined earlier. If you run your server again and make a request using curl, you will now see additional output in your terminal showing when each request begins and ends. This confirms that the middleware is being executed for every incoming request. Middleware becomes even more useful when you combine multiple middleware functions together. Each middleware can wrap another, forming a chain of processing steps. For example, you might want to add a custom response header in addition to logging requests. You can define another middleware function like this: This middleware sets a response header before passing the request to the next handler. To apply both middleware functions, you can wrap them around your handler in sequence: In this setup, each request first passes through the logging middleware, then through the header middleware, and finally reaches the handler associated with the requested route. Once the handler finishes, control returns back through the middleware in reverse order. This layered approach allows you to add shared behavior to your application without modifying each handler individually. Middleware is commonly used in Go applications to handle concerns such as logging, authentication, and request validation. Because it is built on top of the http.Handler interface, it works seamlessly with both the standard library and third-party routers. By structuring your application in this way, your handler functions can remain focused on generating responses, while middleware takes care of reusable logic that applies across multiple routes. One of the ways a user is able to influence the HTTP response they get back from an HTTP server is by using the query string. The query string is a set of values added to the end of a URL. It starts with a ? character, with additional values added using & as a delimiter. Query string values are commonly used as a way to filter or customize the results an HTTP server sends as a response. For example, one server may use a results value to allow a user to specify something like results=10 to say they’d like to see 10 items in their list of results. In this section, you will update your getRoot handler function to use its *http.Request value to access query string values and print them to the output. First, open your main.go file and update the getRoot function to access the query string with the r.URL.Query method. Then, update the main method to remove serverTwo and all its associated code since you’ll no longer need it: In the getRoot function, you use the r.URL field of getRoot’s *http.Request to access properties about the URL being requested. Then you use the Query method of the r.URL field to access the query string values of the request. Once you’re accessing the query string values, there are two methods you can use to interact with the data. The Has method returns a bool value specifying whether the query string has a value with the key provided, such as first. Then, the Get method returns a string with the value of the key provided. In theory, you could always use the Get method to retrieve query string values because it will always return either the actual value for the given key or an empty string if the key doesn’t exist. For many uses, this is good enough — but in some cases, you may want to know the difference between a user providing an empty value or not providing a value at all. Depending on your use case, you may want to know whether a user has provided a filter value of nothing, or if they didn’t provide a filter at all. If they provided a filter value of nothing you may want to treat it as “don’t show me anything,” whereas not providing a filter value would mean “show me everything.” Using Has and Get will allow you to tell the difference between these two cases. In your getRoot function, you also updated the output to show the Has and Get values for both first and second query string values. Now, update your main function to go back to using one server again: In the main function, you removed references to serverTwo and moved running the server (formerly serverOne) out of a goroutine and into the main function, similar to how you were running http.ListenAndServe earlier. You could also change it back to http.ListenAndServe instead of using an http.Server value since you only have one server running again, but by using http.Server, you would have less to update if you wanted to make any additional customizations to the server in the future. Now, once you’ve saved your changes, use go run to run your program again: Your server will start running again, so swap back to your second terminal to run a curl command with a query string. In this command you’ll need to surround your URL with single quotes ('), otherwise your terminal’s shell may interpret the & symbol in the query string as the “run this command in the background” feature that many shells include. In the URL, include a value of first=1 for first, and second= for second: The output will look like this: You’ll see the output from the curl command hasn’t changed from previous requests. If you switch back to your server program’s output, however, you’ll see the new output includes the query string values: The output for the first query string value shows the Has method returned true because first has a value, and also that Get returned the value of 1. The output for second shows that Has returned true because second was included, but the Get method didn’t return anything other than an empty string. You can also try making different requests by adding and removing first and second or setting different values to see how it changes the output from those functions. Once you’re finished, press CONTROL+C to stop your server. In this section you updated your program to use only one http.Server again, but you also added support for reading first and second values from the query string for the getRoot handler function. Using the query string isn’t the only way for users to provide input to an HTTP server, though. Another common way to send data to a server is by including data in the request’s body. In the next section, you’ll update your program to read a request’s body from the *http.Request data. When creating an HTTP-based API, such as a REST API, a user may need to send more data than can be included in URL length limits, or your page may need to receive data that isn’t about how the data should be interpreted, such as filters or result limits. In these cases, it’s common to include data in the request’s body and to send it with either a POST or a PUT HTTP request. In a Go http.HandlerFunc, the *http.Request value is used to access information about the incoming request, and it also includes a way to access the request’s body with the Body field. In this section, you will update your getRoot handler function to read the request’s body. To update your getRoot method, open your main.go file and update it to use io.ReadAll to read the r.Body request field: In this update, you use the io.ReadAll function to read the r.Body property of the *http.Request to access the request’s body. The io.ReadAll function is a utility function that will read data from an io.Reader until it encounters an error or the end of the data. Since r.Body is an io.Reader, you’re able to use it to read the body. Once you’ve read the body, you also updated the fmt.Printf to print it to the output. After you’ve saved your updates, run your server using the go run command: Since the server will continue running until you stop it, go to your other terminal to make a POST request using curl with the -X POST option and a body using the -d option. You can also use the first and second query string values from before as well: Your output will look like the following: The output from your handler function is the same, but you’ll see your server logs have been updated again: In the server logs, you’ll see the query string values from before, but now you’ll also see the This is the body data the curl command sent. Now, stop the server by pressing CONTROL+C. In this section, you updated your program to read a request’s body into a variable you printed to the output. By combining reading the body this way with other functionality, such as encoding/json to unmarshal a JSON body into Go data, you’ll be able to create APIs your users can interact with in a way they’re familiar with from other APIs. Not all data sent from a user is in the form of an API, though. Many websites have forms they ask their users to fill out, so in the next section, you’ll update your program to read form data in addition to the request body and query string you already have. For a long time, sending data using forms was the standard way for users to send data to an HTTP server and interact with a website. Forms aren’t as popular now as they were in the past, but they still have many uses as a way for users to submit data to a website. The *http.Request value in http.HandlerFunc also provides a way to access this data, similar to how it provides access to the query string and request body. In this section, you’ll update your getHello program to receive a user’s name from a form and respond back to them with their name. Open your main.go and update the getHello function to use the PostFormValue method of *http.Request: Now in your getHello function, you’re reading the form values posted to your handler function and looking for a value named myName. If the value isn’t found or the value found is an empty string, you set the myName variable to a default value of HTTP so the page doesn’t display an empty name. Then, you updated the output to the user to display the name they sent, or HTTP if they didn’t send a name. To run your server with these updates, save your changes and run it using go run: Now, in your second terminal, use curl with the -X POST option to the /hello URL, but this time instead of using -d to provide a data body, use the -F 'myName=Sammy' option to provide form data with a myName field with the value Sammy: The output will look like this: In the output above, you’ll see the expected Hello, Sammy! greeting because the form you sent with curl said myName is Sammy. The r.PostFormValue method you’re using in the getHello function to retrieve the myName form value is a special method that only includes values posted from a form in the body of a request. However, an r.FormValue method is also available that includes both the form body and any values in the query string. So, if you used r.FormValue("myName") you could also remove the -F option and include myName=Sammy in the query string to see Sammy returned as well. If you did that without the change to r.FormValue, though, you’d see the default HTTP response for the name. Being careful about where you’re retrieving these values from can avoid potential conflicts in names or bugs that are hard to track down. It’s useful to be more strict and use the r.PostFormValue unless you want the flexibility to put it in the query string as well. If you look back at your server logs you’ll see the /hello request was logged similar to previous requests: To stop the server, press CONTROL+C. In this section, you updated your getHello handler function to read a name from form data posted to the page and then return that name to the user. At this point in your program, a few things can go wrong when handling a request, and your users wouldn’t be notified. In the next section, you’ll update your handler functions to return HTTP status codes and headers. The HTTP protocol uses a few features that users don’t normally see to send data to help browsers or servers communicate. One of these features is called the status code, and is used by the server to give an HTTP client a better idea of whether the server considers the request successful, or whether something went wrong on either the server side, or with the request the client sent. Another way HTTP servers and clients communicate is by using header fields. A header field is a key and value that either a client or server will send to the other to let them know about themselves. There are many headers that are pre-defined by the HTTP protocol, such as Accept, which a client uses to tell the server the type of data it can accept and understand. It’s also possible to define your own by prefixing them with x- and then the rest of a name. In this section, you’ll update your program to make the myName form field of getHello, a required field. If a value isn’t sent for the myName field, your server will send back a “Bad Request” status code to the client and add an x-missing-field header to let the client know which field was missing. To add this feature to your program, open your main.go file one last time and add the validation check to the getHello handler function: In this update, when myName is an empty string, instead of setting a default name of HTTP, you send the client an error message instead. First, you use the w.Header().Set method to set an x-missing-field header with a value of myName in the response HTTP headers. Then, you use the w.WriteHeader method to write any response headers, as well as a “Bad Request” status code, to the client. Finally, it will return out of the handler function. You want to make sure you do this so you don’t accidentally write a Hello, ! response to the client in addition to the error information. It’s also important to be sure you’re setting headers and sending the status code in the right order. In an HTTP request or response, all the headers must be sent before the body is sent to the client, so any requests to update w.Header() must be done before w.WriteHeader is called. Once w.WriteHeader is called, the status of the page is sent with all the headers and only the body can be written to after. Once you’ve saved your updates, you can run your program again with the go run command: Now, use your second terminal to make another curl -X POST request to the /hello URL, but don’t include the -F to send form data. You’ll also want to include the -v option to tell curl to show verbose output so you can see all the headers and output for the request: This time in the output, you’ll see a lot more information as the request is processed because of the verbose output: The first couple of lines in the output show that curl is trying to connect to localhost port 3333. Then, the lines that start with > show the request curl is making to the server. It says curl is making a POST request to the /hello URL using the HTTP 1.1 protocol, as well as a few other headers. Then, it sends an empty body as shown by the empty > line. Once curl sends the request, you can see the response it receives from the server with the < prefix. The first line says that the server responded with a Bad Request, which is also known as a 400 status code. Then, you can see the X-Missing-Field header you set is included with a value of myName. After sending a few additional headers, the request finishes without sending any of the body, which can be seen by the Content-Length (or body) being length 0. If you look back at your server output once more, you’ll see the /hello request the server handled in the output: One last time, press CONTROL+C to stop your server. In this section, you updated your HTTP server to add validation to the /hello form input. If a name isn’t sent as part of the form, you used w.Header().Set to set a header to send back to the client. Once the header is set, you used w.WriteHeader to write the headers to the client, as well as a status code indicating to the client it was a bad request. In many real-world applications, HTTP servers are used to expose APIs that allow clients to send and receive structured data. These APIs are commonly referred to as JSON APIs because they use JSON as the format for both requests and responses. Unlike earlier examples in this tutorial, where the server returned plain text, a JSON API is designed to communicate data in a structured way. This allows different systems such as web applications, mobile apps, or other services, to interact with your server programmatically. In this section, you will create a simple JSON API endpoint that accepts data from a client, processes it, and returns a structured JSON response. To begin, you need to define the structure of the data your API will accept. In Go, this is typically done using a struct. Open your main.go file and add the following definition: This struct represents the expected format of the incoming JSON request. For example, a client might send data like this: The struct tags (such as json:"name") ensure that the JSON keys are correctly mapped to the corresponding struct fields. Next, create a handler function that will act as your API endpoint: This handler demonstrates a common pattern used in JSON APIs. It begins by checking the HTTP method of the request. Since this endpoint is designed to accept data, it only allows POST requests. If a client sends a request using a different method, the server responds with a 405 Method Not Allowed status. Next, the handler reads and decodes the JSON payload from the request body into the Message struct. If the JSON is malformed or does not match the expected structure, the server returns a 400 Bad Request response. After successfully decoding the request, the handler constructs a JSON response. In this example, the response includes a status field and echoes part of the input data. In a real API, this is where you would typically perform application logic such as storing data or querying a database. Before sending the response, the handler sets the Content-Type header to application/json and explicitly writes a 200 OK status. The response is then encoded as JSON and written to the client. Once the handler is defined, register it in your main function: This creates a dedicated API endpoint at /api/message. Using a path prefix such as /api is a common convention that helps distinguish API routes from other types of endpoints. With your server running, you can test the API using curl: If the request is successful, the server will return a JSON response similar to the following: If you send an invalid request—for example, by using the wrong HTTP method or malformed JSON—the server will respond with an appropriate HTTP status code and error message. When a client interacts with this endpoint, the communication follows a structured request-response cycle. The client sends a POST request containing JSON data. The server reads and validates the request, processes the input, and then returns a structured JSON response along with an appropriate HTTP status code. This pattern forms the foundation of most REST-style APIs. By following it, your server can communicate reliably with a wide range of clients while maintaining a clear and predictable interface. By extending your HTTP server to support JSON APIs, you move beyond serving simple responses and begin building services that can integrate with other systems. This is a fundamental step in developing modern backend applications with Go. Throughout this tutorial, you have been building HTTP servers suitable for development and learning purposes. However, when deploying an HTTP server to a production environment, there are several important considerations that can improve the reliability, security, and performance of your application. In this section, you will learn about configuring timeouts, implementing graceful shutdown, enabling HTTPS, and applying other production-ready practices to your Go HTTP server. By default, Go’s HTTP server does not enforce timeouts on connections. This means a slow or malicious client could potentially hold a connection open indefinitely, consuming server resources and potentially leading to resource exhaustion. To prevent this, you should configure explicit timeouts on your http.Server. Go provides several timeout settings that control different phases of the request lifecycle: Open your main.go file and update your server configuration to include these timeouts: In this configuration, the server will wait a maximum of 10 seconds to read a request and 10 seconds to write a response. If a connection is idle (no active request), it will be closed after 120 seconds. These values should be adjusted based on your application’s specific needs. For example, if your server handles large file uploads, you may need to increase ReadTimeout. It’s important to understand the difference between these timeout values. ReadTimeout covers the entire duration from when the connection is accepted to when the request body has been fully read. This means it includes the time taken to read request headers as well. WriteTimeout normally covers the time from the end of the request header read to the end of the response write, but this can vary based on whether the handler reads the request body. For applications that handle long-running requests, such as streaming responses or webhooks, you may need to adjust these timeouts or implement more sophisticated timeout handling within your handler functions using the request’s context. When you need to stop or restart your server, abruptly terminating active connections can result in failed requests and a poor user experience. A graceful shutdown allows the server to stop accepting new connections while completing any in-flight requests before fully shutting down. Go’s http.Server includes a Shutdown method that enables graceful shutdown. This method blocks until all active connections have been closed or until a provided context is canceled. Update your main.go file as follows to implement graceful shutdown: In this implementation, the server starts in a goroutine so that the main function can continue executing. You then set up a channel to listen for interrupt signals such as SIGINT (sent when you press CONTROL+C) or SIGTERM (commonly sent by process managers or orchestration systems). When a shutdown signal is received, the program creates a context with a 30-second timeout and calls server.Shutdown. This tells the server to stop accepting new connections and to wait for existing requests to complete. If all requests finish within the 30-second window, the server shuts down cleanly. If the timeout is reached, the server is forced to close any remaining connections. This pattern is particularly important in production environments where your server may be managed by a process supervisor, container orchestrator like Kubernetes, or a service manager like systemd. These systems expect applications to handle shutdown signals properly. In production environments, you should always serve traffic over HTTPS to encrypt data in transit and protect user privacy. Go’s HTTP server includes built-in support for TLS (Transport Layer Security) through the ListenAndServeTLS method. To enable HTTPS, you need a TLS certificate and a private key. For testing purposes, you can create a self-signed certificate, but for production use you should obtain a certificate from a trusted Certificate Authority (CA) such as Let’s Encrypt. Here’s how to configure your server to use HTTPS: In this example, the server listens on port 8443 (a common port for HTTPS) and uses the certificate file cert.pem and private key file key.pem. Note that the default HTTPS port is 443, but using ports below 1024 typically requires administrator or root privileges. When using HTTPS, you should also configure your server to use secure TLS settings. Go’s default TLS configuration is generally secure, but you can customize it for your specific requirements: This configuration sets the minimum TLS version to 1.2, which disables older, less secure versions of the protocol. The PreferServerCipherSuites setting tells the server to prefer its own cipher suite preferences over the client’s, allowing you to enforce the use of strong cipher suites. Security headers are HTTP response headers that instruct browsers to enable various security protections. While these headers don’t prevent attacks on your server directly, they help protect your users by telling their browsers how to handle your content safely. You can add security headers to your responses using middleware: Each of these headers serves a specific security purpose: You should customize these headers based on your application’s specific requirements. For example, if your application needs to be embedded in iframes, you would adjust or remove the X-Frame-Options header. Health check endpoints allow monitoring systems, load balancers, and orchestration platforms to verify that your server is running and ready to handle requests. A basic health check is a simple endpoint that returns a successful status when the server is operational. Add a health check endpoint to your server: This basic health check always returns a successful response. In a more sophisticated implementation, you might check the status of dependencies such as database connections or external services before responding: This enhanced version checks whether the database connection is working before declaring the service healthy. Many production systems distinguish between liveness checks (is the application running?) and readiness checks (is the application ready to serve traffic?). You can implement separate endpoints for each concern. Proper logging is essential for debugging issues in production environments. While the examples in this tutorial have used fmt.Printf for simplicity, production applications should use a structured logging library that supports log levels, timestamps, and structured data. Here’s an example of a logging middleware that records information about each request: This middleware logs the HTTP method, request URI, client address, and request duration for each request. For production use, consider using a structured logging library like zap or logrus that can output logs in JSON format for easier parsing and analysis by log aggregation systems. You should also implement proper error handling throughout your application. Instead of printing errors to the console, log them with appropriate context, and return meaningful error messages to clients without exposing sensitive internal details. Rate limiting protects your server from abuse by restricting the number of requests a client can make within a given time window. While implementing a full rate limiting solution is beyond the scope of this tutorial, you can use third-party packages like golang.org/x/time/rate or integrate with external rate limiting services. A basic rate limiting middleware might look like this: This example creates a global rate limiter that allows 10 requests per second with a burst capacity of 20 requests. In a production system, you would typically want to implement per-client rate limiting based on IP addresses or authentication tokens rather than a single global limit. For high-traffic production environments, you may need to optimize your server’s performance. Go’s HTTP server is already quite performant by default, but there are several techniques you can use to improve performance further: First, ensure your handler functions are efficient and avoid unnecessary work. Use connection pooling for database connections, cache frequently accessed data, and minimize memory allocations in hot code paths. Second, consider enabling HTTP/2, which Go’s server supports automatically when using TLS. HTTP/2 can improve performance through features like request multiplexing and header compression. Third, if your application serves static files, consider using a Content Delivery Network (CDN) or a reverse proxy like nginx to handle static content, allowing your Go server to focus on dynamic requests. Finally, use profiling tools like pprof to identify performance bottlenecks in your application. Go includes excellent built-in profiling support that can help you understand where your application is spending time and using memory. When deploying your Go HTTP server to production, consider using a process manager or container orchestration system to ensure your application stays running and can be easily updated. Options include: You should also implement monitoring and alerting to track your server’s health and performance over time. Services like Prometheus, Grafana, or cloud-based monitoring solutions can help you identify issues before they impact users. Finally, always test your production configuration thoroughly in a staging environment that mirrors your production setup as closely as possible. This helps catch configuration issues, performance problems, and potential security vulnerabilities before they affect real users. By implementing these production considerations, you can build Go HTTP servers that are secure, reliable, and performant in real-world environments. To create a simple HTTP server in Go, you need to use the net/http package from the standard library. First, define a handler function that accepts http.ResponseWriter and *http.Request parameters. Then, register this handler using http.HandleFunc() and start the server with http.ListenAndServe(): This creates a server that listens on port 8080 and responds with “Hello, World!” to all requests. For production use, you should create your own http.ServeMux instead of using the default one and configure an http.Server with appropriate timeouts. The net/http package is part of Go’s standard library and provides all the functionality needed to build HTTP clients and servers. It includes types and functions for handling HTTP requests and responses, routing, cookies, headers, and more. The package implements the HTTP/1.1 and HTTP/2 protocols and integrates seamlessly with Go’s concurrency features. Key components include http.Server for creating servers, http.ServeMux for routing requests, http.Handler and http.HandlerFunc for defining request handlers, and http.Client for making HTTP requests. Because it’s part of the standard library, you don’t need to install any external dependencies to build fully functional web servers in Go. No, you don’t need a framework to build a web server in Go. The net/http package in the standard library provides everything necessary to create production-ready HTTP servers. This includes routing, middleware support through the http.Handler interface, request parsing, response writing, and TLS support. However, as your application grows more complex, you may benefit from third-party routers or frameworks that provide additional features such as parameterized routes (like /users/{id}), easier middleware chaining, and built-in helpers for common tasks. Popular options include chi, gorilla/mux, echo, and gin. These tools are designed to work with the standard library rather than replace it, so you can adopt them incrementally as your needs evolve. For learning and small to medium applications, the standard library is often sufficient and has the advantage of having no external dependencies. To return JSON from a Go HTTP server, use the encoding/json package to encode your data and set the appropriate Content-Type header. First, define a struct to represent your data, then use json.NewEncoder() to write the JSON response: Always set the Content-Type header to application/json before writing the response. You can also use json.Marshal() if you need the JSON as a byte slice before writing it. Remember to handle encoding errors appropriately in production code. In Go, you add routes by registering handler functions with a server multiplexer. The simplest approach is to use http.HandleFunc() to associate a URL path with a handler function: Each call to HandleFunc() creates a new route. The http.ServeMux supports exact path matching and prefix-based routing. Paths ending with a slash match all URLs with that prefix, while paths without a trailing slash match only that exact path. For more complex routing needs, such as URL parameters (like /users/123), HTTP method-based routing, or route grouping, consider using a third-party router like chi or gorilla/mux. These routers implement the same http.Handler interface, so they integrate seamlessly with the standard library. Go HTTP servers don’t have a default port. You must explicitly specify the port when calling http.ListenAndServe(). The port is provided as part of the address string, such as ":8080" or "localhost:3000". Common conventions include: If you specify just ":8080", the server listens on all available network interfaces. To restrict it to localhost, use "127.0.0.1:8080" or "localhost:8080". Yes, Go is excellent for building web servers. The language was designed with network services in mind and offers several advantages: Many companies use Go for high-performance web services, including Google, Uber, Dropbox, and Docker. It’s particularly well-suited for building APIs, microservices, and backend systems that need to handle significant traffic with low latency. In this tutorial, you created a new Go HTTP server using the net/http package in Go’s standard library. You then updated your program to use a specific server multiplexer and multiple http.Server instances. You also updated your server to read user input via query string values, the request body, and form data, and learned how to return form validation information to the client using custom HTTP headers and status codes. Additionally, you implemented middleware to add reusable request processing logic, built a JSON API endpoint to handle structured data, and explored production-ready practices including server timeouts, graceful shutdown, HTTPS configuration, security headers, health checks, logging, and rate limiting. These concepts form the foundation for building robust, scalable HTTP services in Go. One good thing about the Go HTTP ecosystem is that many frameworks are designed to integrate neatly into Go’s net/http package instead of reinventing a lot of the code that already exists. The chi project is a good example of this. The server multiplexer built into Go is a good way to get started with an HTTP server, but it lacks a lot of advanced functionality a larger web server may need. Projects such as chi are able to implement the http.Handler interface in the Go standard library to fit right into the standard http.Server without needing to rewrite the server portion of the code. This allows them to focus on creating middleware and other tooling to enhance what’s available instead of working on the basic functionality. In addition to projects like chi, the Go net/http package also includes a lot of functionality not covered in this tutorial. To explore more about working with cookies or serving HTTPS traffic, the net/http package is a good place to start. This tutorial is also part of the DigitalOcean How to Code in Go series. The series covers a number of Go topics, from installing Go for the first time to how to use the language itself. For more Go- and web server-related tutorials, check out the following articles: Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases. Learn more about our products Go (or GoLang) is a modern programming language originally developed by Google that uses high-level syntax similar to scripting languages. It is popular for its minimal syntax and innovative handling of concurrency, as well as for the tools it provides for building native binaries on foreign platforms. Browse Series: 53 tutorials Kristin is a life-long geek and enjoys digging into the lowest levels of computing. She also enjoys learning and tinkering with new technologies. With over 6 years of experience in tech publishing, Mani has edited and published more than 75 books covering a wide range of data science topics. Known for his strong attention to detail and technical knowledge, Mani specializes in creating clear, concise, and easy-to-understand content tailored for developers. This textbox defaults to using Markdown to format your answer. You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link! Thanks for the tutorial, there appears to be an error in this text: “One other change is adding a BaseContext function. BaseContext is a way to change parts of the context.Context that handler functions receive when they call the Context method of *http.Request. In the case of your program, you’re adding the address the server is listening on (l.Addr().String()) to the context with the key serverAddr, which will then be printed to the handler function’s output.” the code snippet is this: I think in the text: “… to the context with the key serverAddr…” I bet you mean keyServerAddr there, right? Please complete your information! Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation. Full documentation for every DigitalOcean product. The Wave has everything you need to know about building a business, from raising funding to marketing your product. Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter. New accounts only. By submitting your email you agree to our Privacy Policy Scale up as you grow — whether you're running one virtual machine or ten thousand. Sign up and get $200 in credit for your first 60 days with DigitalOcean.* *This promotional offer applies to new accounts only.