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