Golang Code Examples

HTTP Server

05 Aug 2014

Description

This example demonstrates how to create an HTTP web server.

We read static html files from the local file system as well as use a go template to display html pages.

Our test harness demonstrates the following:

  • Help (this page)
  • File Server (dir value from config file)
  • Redirect (to example.com)
  • Debug Info (POST form)
  • Debug Info (GET request)
  • Ajax Callback
  • Function Adapter


Golang Features

This golang code sample demonstrates the following go language features:

  • url routing via http.NewServeMux
  • go templates
  • read local .html files via ioutil.ReadFile
  • error handling
    • return 500 errors via http.Error
    • quick and dirty error handling via panic
  • response header manipulation via http.ResponseWriter
  • cookie manipulation via http.Cookie
  • set MIME type via response.Header
  • regex used to extract URL path target
  • error logging

3rd Party Libraries

github.com/l3x/jsoncfgo

  • to read application config settings
  • to load global users hash

github.com/go-goodies/go_oops

  • to store global users hash in singleton object

Input Files

webserver-config.json

{
   "host": "localhost",
   "port": 8080,
   "dir": "www/",
   "redirect_code": 307
}


users.json

{
   "joesample": {
      "firstname": "joe",
      "lastname": "sample"
   },
   "alicesmith": {
      "firstname": "alice",
      "lastname": "smith"
   },
   "bobbrown": {
      "firstname": "bob",
      "lastname": "brown"
   }
}

help.html

<!doctype html>
<html>
<head>
  <meta charset='utf-8'>
  <title>go web server example</title>
  <script 
     src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js">
  </script>
</head>
<body>
  <h1>go web server example</h1>

  <p> <a href="/help">Help (this page)</a> </p>
  <p> <a href="/">File Server (dir value from config file)</a> </p>
  <p> <a href="/redirect">Redirect (to example.com)</a> </p>
  <p> <a href="/notFound">NotFound Handler (should get a 404 error)</a> </p>
  <p> <a href="/debugForm">Debug Info (POST form)</a> </p>
  <p> <a href="/debugQuery?firstname=cindy&lastname=sample">Debug Info (GET request)</a> </p>
  <p> <a href="/ajax">Ajax Callback</a> </p>

</body>
</html>

form.html

<!doctype html>
<html>
<head>
  <meta charset='utf-8'>
  <title>go web server example</title>
</head>
<body>

  <form method="POST" action="" name="frmTest">
      <input id="firstname" name="firstname" placeholder="First Name" required="" type="text" value="joe">
      <input id="lastname" name="lastname" placeholder="Last Name" required="" type="text">
  </form>

</body>
</html>

ajax.html

<!doctype html>
<html>
<head>
    <meta charset='utf-8'>
    <title>go server example</title>

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>

    <script>
    var ajaxHandler, ajax_handler, ajax_request, cookieNameForUsername, isUserNameValid, userName, username;

    cookieNameForUsername = "testapp-username";
    username = $.cookie(cookieNameForUsername);

    ajaxHandler = function(json) {
      console.log('json.name', json.name);
      $("#full-name").html(json.name);
    };

    $(function() {
      if ((typeof username != 'undefined') && (username.length > 0)) {
        $("#not-found-msg").html('');
        $.get("/user/" + username, ajaxHandler, "json")
        .fail(function() {
            $("#not-found-msg").html('Enter a valid username in the <strong>Debug Info (POST form)</strong>');
         });
      } else {
        $("#not-found-msg").html('Enter a valid username in the <strong>Debug Info (POST form)</strong>');
        console.log("Invalid username (" + username + ") found in cookie: " + cookieNameForUsername);
      }
    });
    </script>
</head>
<body>
<h1>Ajax Example</h1>

<div id='fullname'>
    <strong>The user's full name is:</strong> <span id="full-name">?</span>
</div>
<br/>
<div id='not-found-msg'></div>

</body>
</html>

Code Example

In this code example we demonstrate how to use simple jsoncfgo functions to read a json-based configuration file.

package main

import (
    "fmt"
    "log"
    "errors"
    "net/http"
    "io/ioutil"
    "html/template"
    "regexp"
    "encoding/json"
    "github.com/l3x/jsoncfgo"
    "github.com/go-goodies/go_oops"
)

var Dir string
var Users jsoncfgo.Obj
var AppContext *go_oops.Singleton

func HtmlFileHandler(response http.ResponseWriter, request *http.Request, filename string){
    response.Header().Set("Content-type", "text/html")
    webpage, err := ioutil.ReadFile(Dir + filename)  // read whole the file
    if err != nil {
        http.Error(response, fmt.Sprintf("%s file error %v", filename, err), 500)
    }
    fmt.Fprint(response, string(webpage));
}

func HelpHandler(response http.ResponseWriter, request *http.Request){
    HtmlFileHandler(response, request, "/help.html")
}

func AjaxHandler(response http.ResponseWriter, request *http.Request){
    HtmlFileHandler(response, request, "/ajax.html")
}

func printCookies(response http.ResponseWriter, request *http.Request) {
    cookieNameForUsername := AppContext.Data["CookieNameForUsername"].(string)
    fmt.Println("COOKIES:")
    for _, cookie := range request.Cookies() {
        fmt.Printf("%v: %v\n", cookie.Name, cookie.Value)
        if cookie.Name == cookieNameForUsername {
            SetUsernameCookie(response, cookie.Value)
        }
    }; fmt.Println("")
}

func UserHandler(response http.ResponseWriter, request *http.Request){
    response.Header().Set("Content-type", "application/json")
    // json data to send to client
    data := map[string]string { "api" : "user", "name" : "" }
    userApiURL := regexp.MustCompile(`^/user/(\w+)$`)
    usernameMatches := userApiURL.FindStringSubmatch(request.URL.Path)
    // regex matches example: ["/user/joesample", "joesample"]
    if len(usernameMatches) > 0 {
        printCookies(response, request)
        var userName string
        userName = usernameMatches[1]  // ex: joesample
        userObj := AppContext.Data[userName]
        fmt.Printf("userObj: %v\n", userObj)
        if userObj == nil {
            msg := fmt.Sprintf("Invalid username (%s)", userName)
            panic(errors.New(msg))
        } else {
            // Send JSON to the client
            thisUser := userObj.(jsoncfgo.Obj)
            fmt.Printf("thisUser: %v\n", thisUser)
            data["name"] = thisUser["firstname"].(string) + " " + thisUser["lastname"].(string)
        }
        json_bytes, _ := json.Marshal(data)
        fmt.Printf("json_bytes: %s\n", string(json_bytes[:]))
        fmt.Fprintf(response, "%s\n", json_bytes)

    } else {
        http.Error(response, "404 page not found", 404)
    }
}

func SetUsernameCookie(response http.ResponseWriter, userName string){
    // Add cookie to response
    cookieName := AppContext.Data["CookieNameForUsername"].(string)
    cookie := http.Cookie{Name: cookieName, Value: userName}
    http.SetCookie(response, &cookie)
}

func DebugFormHandler(response http.ResponseWriter, request *http.Request){

    printCookies(response, request)

    err := request.ParseForm()  // Parse URL and POST data into request.Form
    if err != nil {
        http.Error(response, fmt.Sprintf("error parsing url %v", err), 500)
    }

    // Set cookie and MIME type in the HTTP headers.
    fmt.Printf("request.Form: %v\n", request.Form)
    if request.Form["username"] != nil {
        cookieVal := request.Form["username"][0]
        fmt.Printf("cookieVal: %s\n", cookieVal)
        SetUsernameCookie(response, cookieVal)
    }; fmt.Println("")

    templateHandler(response, request)
    response.Header().Set("Content-type", "text/plain")

    // Send debug diagnostics to client
    fmt.Fprintf(response, "<table>")
    fmt.Fprintf(response, "<tr><td><strong>request.Method    </strong></td><td>'%v'</td></tr>", request.Method)
    fmt.Fprintf(response, "<tr><td><strong>request.RequestURI</strong></td><td>'%v'</td></tr>", request.RequestURI)
    fmt.Fprintf(response, "<tr><td><strong>request.URL.Path  </strong></td><td>'%v'</td></tr>", request.URL.Path)
    fmt.Fprintf(response, "<tr><td><strong>request.Form      </strong></td><td>'%v'</td></tr>", request.Form)
    fmt.Fprintf(response, "<tr><td><strong>request.Cookies() </strong></td><td>'%v'</td></tr>", request.Cookies())
    fmt.Fprintf(response, "</table>")

}

func DebugQueryHandler(response http.ResponseWriter, request *http.Request){

    // Set cookie and MIME type in the HTTP headers.
    response.Header().Set("Content-type", "text/plain")

    // Parse URL and POST data into the request.Form
    err := request.ParseForm()
    if err != nil {
        http.Error(response, fmt.Sprintf("error parsing url %v", err), 500)
    }

    // Send debug diagnostics to client
    fmt.Fprintf(response, " request.Method     '%v'\n", request.Method)
    fmt.Fprintf(response, " request.RequestURI '%v'\n", request.RequestURI)
    fmt.Fprintf(response, " request.URL.Path   '%v'\n", request.URL.Path)
    fmt.Fprintf(response, " request.Form       '%v'\n", request.Form)
    fmt.Fprintf(response, " request.Cookies()  '%v'\n", request.Cookies())
}

func templateHandler(w http.ResponseWriter, r *http.Request) {
    t, _ := template.New("form.html").Parse(form)
    t.Execute(w, "")
}

func formHandler(w http.ResponseWriter, r *http.Request) {
    log.Println(r.Form)
    templateHandler(w, r)
}

var form = `
<h1>Debug Info (POST form)</h1>
<form method="POST" action="" name="frmTest">
<div>
    <label for="username">User Name</label>
    <input id="username" name="username" placeholder="joesample, alicesmith, or bobbrown" required="" type="text"
size="50">
</div>
<div><input type="submit" value="Submit"></div>
</form>

</form>
`

func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("errorHandler...")
        err := f(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            log.Printf("handling %q: %v", r.RequestURI, err)
        }
    }
}

func doThis() error { return nil }
func doThat() error { return errors.New("ERROR - doThat") }

func wrappedHandler(w http.ResponseWriter, r *http.Request) error {
    log.Println("betterHandler...")
    if err := doThis(); err != nil {
        return fmt.Errorf("doing this: %v", err)
    }

    if err := doThat(); err != nil {
        return fmt.Errorf("doing that: %v", err)
    }
    return nil
}


func main() {
    cfg := jsoncfgo.Load("/Users/lex/dev/go/data/webserver/webserver-config.json")

    host := cfg.OptionalString("host", "localhost")
    fmt.Printf("host: %v\n", host)

    port := cfg.OptionalInt("port", 8080)
    fmt.Printf("port: %v\n", port)

    Dir = cfg.OptionalString("dir", "www/")
    fmt.Printf("web_dir: %v\n", Dir)

    redirect_code := cfg.OptionalInt("redirect_code", 307)
    fmt.Printf("redirect_code: %v\n\n", redirect_code)

    mux := http.NewServeMux()

    fileServer := http.Dir(Dir)
    fileHandler := http.FileServer(fileServer)
    mux.Handle("/", fileHandler)

    rdh := http.RedirectHandler("http://example.org", redirect_code)
    mux.Handle("/redirect", rdh)
    mux.Handle("/notFound", http.NotFoundHandler())

    mux.Handle("/help", http.HandlerFunc( HelpHandler ))

    mux.Handle("/debugForm", http.HandlerFunc( DebugFormHandler ))
    mux.Handle("/debugQuery", http.HandlerFunc( DebugQueryHandler ))

    mux.Handle("/user/", http.HandlerFunc( UserHandler ))
    mux.Handle("/ajax", http.HandlerFunc( AjaxHandler ))

    mux.Handle("/adapter", errorHandler(wrappedHandler))

    log.Printf("Running on port %d\n", port)

    addr := fmt.Sprintf("%s:%d", host, port)

    Users := jsoncfgo.Load("/Users/lex/dev/go/data/webserver/users.json")

    joesample := Users.OptionalObject("joesample")
    fmt.Printf("joesample: %v\n", joesample)
    fmt.Printf("joesample['firstname']: %v\n", joesample["firstname"])
    fmt.Printf("joesample['lastname']: %v\n\n", joesample["lastname"])

    alicesmith := Users.OptionalObject("alicesmith")
    fmt.Printf("alicesmith: %v\n", alicesmith)
    fmt.Printf("alicesmith['firstname']: %v\n", alicesmith["firstname"])
    fmt.Printf("alicesmith['lastname']: %v\n\n", alicesmith["lastname"])

    bobbrown := Users.OptionalObject("bobbrown")
    fmt.Printf("bobbrown: %v\n", bobbrown)
    fmt.Printf("bobbrown['firstname']: %v\n", bobbrown["firstname"])
    fmt.Printf("bobbrown['lastname']: %v\n\n", bobbrown["lastname"])

    AppContext = go_oops.NewSingleton()
    AppContext.Data["CookieNameForUsername"] = "testapp-username"
    AppContext.Data["joesample"] = joesample
    AppContext.Data["alicesmith"] = alicesmith
    AppContext.Data["bobbrown"] = bobbrown
    fmt.Printf("AppContext: %v\n", AppContext)
    fmt.Printf("AppContext.Data[\"joesample\"]: %v\n", AppContext.Data["joesample"])
    fmt.Printf("AppContext.Data[\"alicesmith\"]: %v\n", AppContext.Data["alicesmith"])
    fmt.Printf("AppContext.Data[\"bobbrown\"]: %v\n\n", AppContext.Data["bobbrown"])

    err := http.ListenAndServe(addr, mux)
    fmt.Println(err.Error())
}

Notes

You will need to change the configPath to point to a the configuration files on your computer.

Cookies are available after a page refresh.

The log statement prepends the current date and time, e.g., 2014/08/07 19:05:13.

Return Early

It is best practice to return early.

Here, in DebugFormHandler, we return a 500 error as soon as we notice an error:

if err != nil {
    http.Error(response, fmt.Sprintf("error parsing url %v", err), 500)
}

Processing HTTP requests

ServeMuxes and Handlers work together to handle http requests.

NewServeMux allocates and returns a new ServeMux.

ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.

Handle registers the handler for the given pattern.

Handlers are responsible for writing response headers and bodies. A handler must implement the Handler interface:

ServeHTTP(ResponseWriter, *Request)

Patterns name fixed, rooted paths, like "/help.html", or rooted subtrees, like "/user/" (note the trailing slash). Longer patterns take precedence over shorter ones, so that if there are handlers registered for both "/users/" and "/users/images/", the latter handler will be called for paths beginning "/users/images/" and the former will receive requests for any other paths in the "/users/" subtree.

Note that since a pattern ending in a slash names a rooted subtree, the pattern "/" matches all paths not matched by other registered patterns, not just the URL with Path == "/".

Patterns may optionally begin with a host name, restricting matches to URLs on that host only. Host-specific patterns take precedence over general patterns, so that a handler might register for the two patterns "/codesearch" and "codesearch.google.com/" without also taking over requests for "http://www.google.com/".

ServeMux also takes care of sanitizing the URL request path, redirecting any request containing . or .. elements to an equivalent .- and ..-free URL.

Function Adapter

The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers.

Each handler needs to have the same signature.

mux.Handle("/help", http.HandlerFunc( HelpHandler ))

In essence, http.HandlerFunc wraps the handler that is passed to it.

// If f is a function with the appropriate signature, HandlerFunc(f) is a Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

wrappedHandler

We also included a function adapter example.

The last thing we do is visit the function adapter url.

This decorator pattern is frequently seen in middleware implementations.

In this example, we have a function (errorHandler) that calls a function (wrappedHandler) that returns a function.

mux.Handle("/adapter", errorHandler(wrappedHandler))

Often when creating http handlers it is best to use buffers in order to wait until the entire operation completes before setting the http return code. (We would not want to set the return code to "200" if there is a possibility of hitting a server error, which should return a "500" return code.)


Handlers

This example NewServeMux handles eight url patterns:

  • /
  • /redirect
  • /notFound
  • /help
  • /debugForm
  • /debugQuery
  • /user/
  • /ajax
  • /adapter

Go's HTTP package ships with a few functions to generate common handlers, such as FileServer, NotFoundHandler and RedirectHandler.

In the main function we use the http.NewServeMux function to create an empty ServeMux, "mux".

We use the FileHandler function to create the fileHandler, which reads the files a the root directory, "www/".

Next, we use the Handle function to register this with our new ServeMux, so it acts as the handler for all incoming requests with the URL path "/".

The same pattern is applied to the remaining seven handlers.

Finally, we create a new http server and start listening for incoming requests with the ListenAndServe function, passing in the "mux" for it to match requests against.

ListenAndServe

ListenAndServe listens on the TCP network address addr and then calls Serve with handler to handle requests on incoming connections. Handler is typically nil, in which case the DefaultServeMux is used.

This is the ListenAndServe interface definition:

func ListenAndServe(addr string, handler Handler) error

However, our call to ListenAndServe looks like this:

err := http.ListenAndServe(addr, mux)

We were able to do this because mux has a ServeHTTP method, which satisfies the Handler interface.

So, what we have is a chaining of handlers; mux is itself a handler which passes the request on to one of our eight handlers to process.

http.Handle

We could have let the DefaultServeMux object handle http requests, instead of using the mux object.

//mux := http.NewServeMux()
//mux.Handle("/", fileHandler)
http.Handle("/", fileHandler)
//mux.Handle("/redirect", rdh)
http.Handle("/redirect", rdh)
// etc ...
//err := http.ListenAndServe(addr, mux)
err := http.ListenAndServe(addr, nil)

Note that ListenAndServe defaults to using the DefaultServeMux if the handler is missing.



Output



2014/08/07 19:05:13 Running on port 8080

host: localhost
port: 8080
web_dir: www/
redirect_code: 307

joesample: map[firstname:joe lastname:sample]
joesample['firstname']: joe
joesample['lastname']: sample

alicesmith: map[firstname:alice lastname:smith]
alicesmith['firstname']: alice
alicesmith['lastname']: smith

bobbrown: map[firstname:bob lastname:brown]
bobbrown['firstname']: bob
bobbrown['lastname']: brown

AppContext: &{map[alicesmith:map[firstname:alice lastname:smith] bobbrown:map[firstname:bob lastname:brown] CookieNameForUsername:testapp-username joesample:map[firstname:joe lastname:sample]]}
AppContext.Data["joesample"]: map[firstname:joe lastname:sample]
AppContext.Data["alicesmith"]: map[firstname:alice lastname:smith]
AppContext.Data["bobbrown"]: map[firstname:bob lastname:brown]

COOKIES:
_ga: GA1.1.199573528.1406055892

request.Form: map[]

COOKIES:
_ga: GA1.1.199573528.1406055892

request.Form: map[username:[joesample]]
cookieVal: joesample

COOKIES:
_ga: GA1.1.199573528.1406055892
testapp-username: joesample


2014/08/05 14:04:35 errorHandler...
2014/08/05 14:04:35 betterHandler...
2014/08/05 14:04:35 handling "/adapter": doing that: ERROR - doThat



References

comments powered by Disqus
The content of this site is licensed under the Creative Commons 3.0License and code is licensed under a BSD license