Servers
The net/http
package provides the ListenAndServer()
function that creates a default server. However, this function does not allow you to define timeouts to manage bad connections or server resources, so you should define custom server.
Server type
Create a custom server with the Server
type. The Server
type is a struct with the following fields:
type Server struct {
Addr string
Handler Handler
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
BaseContext func(net.Listener) context.Context
ConnContext func(ctx context.Context, c net.Conn) context.Context
}
HTTP handlers
Servers receive requests, dispatch those requests to the handler that is registered to the receiving path, and the handler sends a response. In Go, each incoming request is served in its own goroutine, which means each request is handled concurrently.
You create the server, then use HandleFunc
to register routes to handler functions. Then you use HandlerFunc
to define the handler function for the route.
http.Handler
is an interface that has theServeHTTP(w ResponseWriter, r *Request)
signature.http.HandlerFunc
is an adapter type that lets you define ordinary functions as HTTP handlers. It adds aServerHTTP(w, r)
method to whatever function you pass it.http.HandleFunc
registers a handler with a multiplexer server. It is syntactic sugar–it transforms a function to a handler and registers it in one step. It accepts two arguments: the path as astring
, and the handler function. It implementsServeHTTP()
.http.NewServeMux()
returns a custom server. It implementsServeHTTP()
.
The following example registers the homeHandler
twice, but each method is functionally equivalent:
func homeHandler(w http.ResponseWriter, r *http.Request) {
// handler logic
}
func main() {
...
mux.Handle("/", http.HandlerFunc(homeHandler))
mux.HandleFunc("/", homeHandler)
}
Handlers as methods
Create handlers and helpers as methods on the application struct. This allows the handlers to access any application dependencies, like loggers:
// main.go
type application struct {
log *log.Logger,
...
}
// handlerName.go
func (app *application) handlerName(w, r) {...}
Requests
URL query strings
Sometimes requests pass information as key-value pairs in the URL:
http://localhost:4000/snippet/view?id=123
The values after the ?
are query strings, and they take the form of key
=value
. You can access the key
portion of the query string and retrieve the value
:
id, err := r.URL.Query().Get("id")
The proceeding example uses the Query()
method on the request’s URL
field. Query()
returns a Values
type, which is essenitally a map that holds all the query parameter key-value pairs. The Values
type has the Get()
method that returns a string
value associated with the key
provided to Get()
.
Responses
Responses are made with the http.ResponseWriter
interface, which is usually represented with a w
.
It is common to use fmt.Fprintf()
to write a response:
fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
- http.Error(writer, error message, status code)
- Writes an error message on the writer. You can use helper methods like
http.StatusText
. For example:
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- http.NotFound(w, r)
- Replies with a
404 Not Found
status code.
Headers
Go provides the following headers automatically:
Date
Content-Length
Content-Type
(see w.Header().Set())
- w.Header().Set(
header-name
,header-value
) - Adds a custom header to the HTTP response in the
header-name
:header-value
format. A common example is setting theContent-Type
header when you send JSON data:
w.Header().Set("Content-Type", "application/json")
You must write the w.Header().Set()
method before w.WriteHeader()
or w.Write()
.
- w.WriteHeader(
status-code
) - Writes a status code to the response. You should call this method only once.
If you do not call this method before
w.Write([]bytes())
, then theWrite
method returns a200 OK
status code automatically.w.WriteHeader
is not commonly used. You usually pass the writer (w
) value to anhttp
package helper function.
Constants
The http
package provides constants for request methods and status code. These constants improve readability and reduce runtime errors that result from typos. For example:
func snippetCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
w.Write([]byte("Create a new snippet..."))
}
Routers
You can create a router with Go’s standard library or a 3rd party package.
Go router
The Go standard library does not support naed parameters in URLs, so you might consider a 3rd-party solution for servers with complex routes.
By default, Go supports two types of URL patterns:
- fixed path
/path/endpoint
- A fixed path does not have a trailing slash (
/
). A request must match a fixed path exactly. - subtree path
/path/bucket/
- A subtree path has a trailing slash. A request must match only the start of the subtree path. Think of subtree paths with a wildcard appended. For example,
/path/bucket/*
.
Create a Go router with these components:
http.Handler
type to satisfy the server’sHandler
field.Server
struct that wraps thehttp.Handler
type.- Constructor that returns an instance of the struct that wraps the new type.
- A method on the
Server
that registers routes. - One or more constants that define the routes.
The following example creates all required components:
const (
shorteningRoute = "/s"
resolveRoute = "/r/"
healthCheckRoute = "/health"
)
// mux is an unexported http.Handler
type mux http.Handler
// Server is a custom server type.
type Server struct {
mux // the server only exports ServeHTTP
}
// NewServer returns an instance of the custom Server type.
func NewServer() *Server {
var s Server
s.registerRoutes()
return &s
}
func (s *Server) registerRoutes() {
mux := http.NewServeMux()
mux.Handle(shorteningRoute, httpio.Handler(s.shorteningHandler))
mux.Handle(resolveRoute, httpio.Handler(s.resolveHandler))
mux.HandleFunc(healthCheckRoute, s.healthCheckHandler)
s.mux = mux
}
3rd-party router
This example uses the julienschmidt router. This router has a few advantages over the Go standard library router:
- Supports named parameters.
- Validates routes. For example, this router matches the home path (
"/"
) path exactly, so no need for manual checks. - Easily create
404
error handlers.
To create a router, use the New()
method. You can register handlers with the Handler()
or HandlerFunc()
method:
router := httprouter.New()
router.HandlerFunc(http.MethodGet, "/snippet/view/:id", app.snippetView)
:id
is a named parameter–it acts as a wildcard for the path segment that it represents.
Always use the HTTP method constants when you register a route.
You can also use a parameter that matches everything, which is useful when registering static files:
fileServer := http.FileServer(http.Dir("./ui/static/"))
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
404 handler
The router.NotFound
handler is called when no matching route is found. You can assign it an http.HandlerFunc(func{w, r})
to handle 404
responses:
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
Middleware and router return
Use alice.New()
to chain middleware in a reader-friendly format:
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
Next, return the router:
return standard.Then(router)
Placeholder routers
When you create a router, it is a good practice to register placeholder handlers that return dummy plaintext responses. This helps you set up the routing infrastructure while managing the business logic at a later time.
In your handlers file, add the placeholder routes:
func (app *application) userSignup(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Display an HTML form for signing up a new user")
}
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Create a new user...")
}
...
In your routes file (where you register your routes), register the routes:
router := httprouter.New()
...
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.Then(app.userSignupPost()))
Writing a basic server
There are two important concepts that are central to creating a custom server in Go:
Server
type: listens for client connections and routes incoming requests in a goroutine to a type that implements theHandler
interface.Handler
interface: contains one method,ServeHTTP(ResponseWriter, *Request)
.
You can create a server with any type as long as it implements Handler
interface. A server should have the following:
- One or more registered routes with an associated function to handle requests to that route.
- A server multiplexer that can manage multiple registered routes.
Directory structure
A suggested server directory structure:
.
├── cmd
│ └── server
│ └── server.go
├── internal
│ └── helpers
│ ├── handler.go
│ └── helpers.go
├── errors
│ └── errors.go
└── service-name
├── server.go
├── server_test.go
└── service.go
In the preceding example:
cmd/serverdir/server.go
contains themain
method. This is where you create a logger and execute the server’sListenAndServe()
method.internal/helpers/*
contains private code such as JSON formatters and handlers for the handler chaining pattern.errors/errors.go
contains custom errors.server-name/server.go
contains the service’s server implementation, including the constructor, routes, etc.service-name/server_test.go
testsserver.go
with thehttptest.NewRecorder()
method.server-name/service.go
contains the service’s business logic.
Define the custom Server type
In the project root, create a subdirectory and file called json-server/server.go
:
mkdir json-server
touch json-server/server.go
The Server
type must implement the Handler
interface, so embed the Handler
interface in the Server type:
type mux http.Handler
type Server struct {
mux
}
Interface embedding elevates the interface methods to the containing type, so the Server
implements the Handler
’s ServeHTTP
method.
Create the main method
In the project root, create the main
server.go
file:mkdir -p cmd/server/server.go
In the
main
method, define theaddress
that the server listens on inhost:port
format. If you provide only theport
, then the server listens on all available network interfaces. This means thathost
is important only if your machine has multiple network interfaces.Additionally, define a
timeout
duration. Thetimeout
makes the server return aftertimeout
seconds if the response is not complete:const ( addr = "localhost:8080" timeout = 10 * time.Second )
In some circumstances, you will see
:http
or:http-alt
in place of a port number. This means the server looks up the port number in theetc/services
file.Create a new multiplexer server:
server := server.NewServer()
Create the server. Assign the
address
andtimeout
variables. ForHandler
, useTimeoutHandler
so you can assign the timeout value:server := &http.Server{ Addr: addr, Handler: http.TimeoutHandler(server, timeout, "timeout"), // runs server for timeout seconds ReadTimeout: timeout, // max time for reading the entire request }
Start the server. The normal behavior for stopping a server returns the
http.ErrServerClosed
error, so check for that error and return a message:if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { fmt.Fprintln(os.Stderr, "server closed unexpectedly:", err) }
Example 2
customServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", *host, *port),
Handler: MuxFunc(*todoFile),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
Start the server with ListenAndServe
:
if err := s.ListenAndServe(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Timeouts
Add timeouts to the server instance. The following timeouts apply to all requests:
srv := &http.Server{
...
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
In the preceding configuration:
IdleTimeout
closes all keep-alive connections after one minute. A keep-alive is HTTP connection reuse, where a single TCP connection is used to send and receive multiple HTTP requests and responses rather than opening new connections.Always set an
IdleTimeout
for the server.ReadTimeout
sets the amount of time that the request headers and body are read after the request is first accepted. If the read operation exceeds the setting, the connection is closed.By default,
IdleTimeout
uses the same setting asReadTimout
if it is not explicitly set. Again–always setIdleTimeout
on the server.WriteTimeout
closes the connection if the server tries to write to the connection after the set length. This setting’s timeout period behavior depends upon the protocol:- HTTP: Times out setting seconds after the request header is read.
- HTTPS: Times out setting seconds after the request is accepted.
This setting does not impact long-running handlers–it impacts how long the handler writes from its buffer to the connection when it returns.
Static files
To server static files (files that the server does not process), you have to create a Handler
with the FileServer()
function. The FileServer()
handler serves HTTP requests with the files stored in the path that you pass as a function argument:
staticFiles := http.FileServer(http.Dir("/public"))
When you register the file server handle, you have to strip the path pattern from the request URL, or the server attempts to serve files from an invalid path and returns a 404:
mux.Handle("/static", http.StripPrefix("/static", staticFiles))
The preceding examples create a Handler
named staticFiles
that serves HTTP requests with the contents of the /public
directory. When you register the staticFiles
, you are telling the multiplexer that when there is a request to the /static
path, strip /static
from the request URL, and then search the /public
directory for a file that matches the request.
Directory listings
After you create a file server, users can access the static files in a browser by going to /static/path/to/assets/
. To disable this, create a blank index.html
file in each subdirectory in the /static/
directory.
Middleware
The following sections detail how to create, register, and chain middlewares with Go’s standard library. You can use the justinas/alice package to simplify middleware code.
Middleware is code that executes on a request before or after your handlers. It is functionality that you want to share among your HTTP request handlers. For example:
- Logging
- Response compression
- Serving files from a cache
You chain middleware. After one middleware function executes, it calls the ServeHTTP()
method on the next handler until a response returns.
Middleware uses the following pattern:
func myMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// TODO: Execute our middleware logic here...
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
The pattern does the following:
myMiddleware
wraps thenext
handler that you pass as an argument.fn
is a closure that has access to any values in thenext
handler’s scope. It returnsnext.ServerHTTP()
.myMiddleware
converts the closure to a handler and returns.
This pattern can be simplified as follows:
func myMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: Execute our middleware logic here...
next.ServeHTTP(w, r)
})
}
Positioning
If you want the middleware to execute on all requests, place it before the server mux:
myMiddleware -> mux -> handler
If you want the middleware to execute on a specific request, place it after the server mux, but before the request handler:
mux -> myMiddleware -> handler
Security headers
Create a new /cmd/web/middleware.go
file and add a middleware function that adds secure headers to each request. These headers satisfy OWASP standards:
func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protections", "0")
next.ServeHTTP(w, r)
})
}
The previous example adds these headers:
- Content-Security-Policy (CSP) retricts the resources that you site can access.
During development, CSP headers are a common cause of blocked resource loads.
- Referrer-Policy includes the site URL in a
Referrer
header when users navigate away from your site. - X-Content-Type-Options: nosniff prevents content-sniffing attacks.
- X-Frame-Options: deny prevents clickjacking in old browsers that do not support CSP.
- X-XSS-Protection: 0 disables blocking of cross-site scripting attacks because we are using CSP headers.
Request logging
Log all requests before they are dispatched to a handler:
func (app *application) logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.infoLog.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
next.ServeHTTP(w, r)
})
}
The preceding middleware is added as a method on the application so that it can access the application dependencies, like the infoLog
:
type application struct {
...
infoLog *log.Logger
...
}
Register middleware
If your middleware needs to execute on all routes, register it where you register routes. This example returns the security middleware from the logging middleware, and the security middleware returns the mux
.
// return a handler
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
// register routes
return app.logRequest(secureHeaders(mux))
}
Think of the
mux
as the main handler that you are returning–it is the handler registered to your server struct inmain
–so the other handlers enclose it.
Panics
If there is a panic in a handler, the panic is isolated in the goroutine that runs the handler, so your application does not stop. However, you should handle panics to provide a good user experience.
The following middleware handles panics in a handler goroutine. It recovers from the panic, then sets a Connection: close
header so the server closes the panicked connection.
func (app *application) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a deferred function (which will always be run in the event
// of a panic as Go unwinds the stack).
defer func() {
// Use the builtin recover function to check if there has been a
// panic or not. If there has...
if err := recover(); err != nil {
// Set a "Connection: close" header on the response.
w.Header().Set("Connection", "close")
// Call the app.serverError helper method to return a 500
// Internal Server response.
app.serverError(w, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}
Add this handler to the beginning of the middleware chain so that it recovers from any panic that occurs in any handler:
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
// register routes
return app.recoverPanic(app.logRequest(secureHeaders(mux)))
}
The previous panic handlers recover only when the panic occurs in the same goroutine that runs the handler. If your handler spins off another goroutine that might panic, you can add the following anonymous function to do the work and handle the possible panic:
func myHandler(w http.ResponseWriter, r *http.Request) {
...
// Spin up a new goroutine to do some background processing.
go func() {
defer func() {
if err := recover(); err != nil {
log.Print(fmt.Errorf("%s\n%s", err, debug.Stack()))
}
}()
doSomeBackgroundProcessing()
}()
w.Write([]byte("OK"))
}
Forms
Parse a form with the Request’s .ParseForm()
method. This method stores data from the form in the requests PostForm
field as a map of Values
. In addition, the Request type has a Form
field. The two have the following differences:
PostForm
is populated for onlyPOST
,PATCH
, andPUT
requests.Form
is populated for all requests, including query string parameters. Use theGet()
method to access query string parameters.
The name
attribute of each HTML form element is the key in the map, and you access the value with the .Get()
method. For example:
<input type="text" name="title">
To get the value of this input, use the following:
title := r.PostForm.Get("title")
Consider the following form:
<form action="/snippet/create" method="post">
<div>
<label>Title:</label>
<input type="text" name="title">
</div>
<div>
<label>Content:</label>
<textarea name="content"></textarea>
</div>
<div>
<label>Delete in:</label>
<input type="radio" name="expires" value="365" checked> One Year
<input type="radio" name="expires" value="7"> One Week
<input type="radio" name="expires" value="1"> One Day
</div>
<div>
<input type="submit" value="Publish snippet">
</div>
</form>
The following method parses the form, extracts its values with error checking, then inserts the values in a table. At the end, it redirects the request to a new path with Redirect()
:
type snippetCreateForm struct {
Title string
Content string
Expires int
validator.Validator
}
func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
// First we call r.ParseForm() which adds any data in POST request bodies
// to the r.PostForm map. This also works in the same way for PUT and PATCH
// requests. If there are any errors, we use our app.ClientError() helper to
// send a 400 Bad Request response to the user.
err := r.ParseForm()
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
// The r.PostForm.Get() method always returns the form data as a *string*.
// However, we're expecting our expires value to be a number, and want to
// represent it in our Go code as an integer. So we need to manually covert
// the form data to an integer using strconv.Atoi(), and we send a 400 Bad
// Request response if the conversion fails.
expires, err := strconv.Atoi(r.PostForm.Get("expires"))
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
// Use the r.PostForm.Get() method to retrieve the title and content
// from the r.PostForm map.
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
}
form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
form.CheckField(validator.PermittedInt(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl.html", data)
return
}
id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
if err != nil {
app.serverError(w, err)
return
}
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}
If a form field sends multiple values, you have to loop through the PostForm
. For example:
<input type="checkbox" name="items" value="foo"> Foo
<input type="checkbox" name="items" value="bar"> Bar
<input type="checkbox" name="items" value="baz"> Baz
for i, item := range r.PostForm["items"] {
fmt.Fprintf(w, "%d: Item %s\n", i, item)
}
ParseForm
cannot parse request bodies that exceed 10MB, or it returns an error. You can change this amount using MaxBytesReader()
:
// Limit the request body size to 4096 bytes
r.Body = http.MaxBytesReader(w, r.Body, 4096)
err := r.ParseForm()
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
When the limit is reached, the server closes the underlying TCP connection.
Validation
See Validation Snippets for Go for field validation patterns.
Validate form values with standard if
clauses and your validation criteria for best practices. A useful pattern is to create a map, and store the field and a discription of any invalid data in the map:
// Initialize a map to hold any validation errors for the form fields.
fieldErrors := make(map[string]string)
// validate fields
// If there are any errors, dump them in a plain text HTTP response and
// return from the handler.
if len(fieldErrors) > 0 {
fmt.Fprint(w, fieldErrors)
return
}
Sessions
Create a session with the scs package with the following steps:
First, create a table to store the session data.
In the main
method:
- Add the session manager to the application struct.
- Create the session manager with
New()
. - Configure the new session manager to use the database.
- Set a lifetime on the session manager.
In routes.go
:
5. Create a new middleware chain.
6. Update any routes with the middleware chain.
In the relevant handlers:
- Add the message or busniess logic into the session context.
In the newTemplateData
helper method:
- Add a field that makes the business logic available to the application when it is present.
In templates.go
:
- Add a field to the
templateData
struct that stores the data that you want to pass to the template.
Update the template to display the dynamic data from templateData
.
Now, the LoadAndSave()
middleware checks each incoming request for a cookie. If a cookie is present, it does the following:
- Reads the token
- Uses the token to retrieve session data from the database
- Adds the session data to the request context so the handlers can use it.
Any changes to session data are updated in the handler request context, and then the middleware updates the changes when it returns.
Shutdown gracefully
You might have concurrent code that is still running when your server fails, and you need to make sure that the process completes before the server stops. You can add a WaitGroup to the application struct, and wait until all goroutines complete before you shut down the server.
Add the WaitGroup to the application struct:
// cmd/api/main.go
type application struct {
config config
logger *jsonlog.Logger
models data.Models
mailer mailer.Mailer
wg sync.WaitGroup
}
Wait for all goroutines to complete before you shutdown the server:
// cmd/api/server.go
func (app *application) serve() error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", app.config.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
shutdownError := make(chan error)
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
s := <-quit
app.logger.PrintInfo("caught signal", map[string]string{
"signal": s.String(),
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Call Shutdown() on the server like before, but now we only send on the
// shutdownError channel if it returns an error.
err := srv.Shutdown(ctx)
if err != nil {
shutdownError <- err
}
// Log a message to say that we're waiting for any background goroutines to
// complete their tasks.
app.logger.PrintInfo("completing background tasks", map[string]string{
"addr": srv.Addr,
})
// Call Wait() to block until our WaitGroup counter is zero --- essentially
// blocking until the background goroutines have finished. Then we return nil on
// the shutdownError channel, to indicate that the shutdown completed without
// any issues.
app.wg.Wait()
shutdownError <- nil
}()
app.logger.PrintInfo("starting server", map[string]string{
"addr": srv.Addr,
"env": app.config.env,
})
err := srv.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
return err
}
err = <-shutdownError
if err != nil {
return err
}
app.logger.PrintInfo("stopped server", map[string]string{
"addr": srv.Addr,
})
return nil
}
Existing docs
Implement the fields that you need when you define a custom server:
Multiplexers
A multiplexer maps incoming requests to the proper handler functions using the request URL. net/http
provides the DefaultServeMux function that returns the default multiplexer, but you should define a custom multiplexer for the following reasons:
- The default registers routes globally.
- You can add dependencies to the routes.
- Custom multiplexers allow integration testing
Router functions
func todoRouter(todoFile string, l sync.Locker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
list := &todo.List{}
l.Lock()
defer l.Unlock()
if err := list.Get(todoFile); err != nil {
replyError(w, r, http.StatusInternalServerError, err.Error())
return
}
if r.URL.Path == "" {
switch r.Method {
case http.MethodGet:
getAllHandler(w, r, list)
case http.MethodPost:
addHandler(w, r, list, todoFile)
default:
message := "Method not supported"
replyError(w, r, http.StatusMethodNotAllowed, message)
}
return
}
id, err := validateID(r.URL.Path, list)
if err != nil {
if errors.Is(err, ErrNotFound) {
replyError(w, r, http.StatusNotFound, err.Error())
return
}
replyError(w, r, http.StatusBadRequest, err.Error())
return
}
switch r.Method {
case http.MethodGet:
getOneHandler(w, r, list, id)
case http.MethodDelete:
deleteHandler(w, r, list, id, todoFile)
case http.MethodPatch:
patchHandler(w, r, list, id, todoFile)
default:
message := "Method not supported"
replyError(w, r, http.StatusMethodNotAllowed, message)
}
}
}
Multiplexer definition:
func newMux(todoFile string) http.Handler {
m := http.NewServeMux()
mu := &sync.Mutex{}
m.HandleFunc("/", rootHandler)
t := todoRouter(todoFile, mu)
m.Handle("/todo", http.StripPrefix("/todo", t))
m.Handle("/todo/", http.StripPrefix("/todo/", t))
return m
}