This page looks best with JavaScript enabled

Building Web Server with Go - Part 4

 ·  ☕ 8 min read
This is part 4 of 09 part series on building web server with go.
Checkout https://www.gophersumit.com/series/web-server/ for more.

forms

HTML forms are used to collect data from user. In this part, we will build simple html form that can be used to submit data and save on server.

For user to be able to submit data, we will have to user html <form> tag with post action. Form tag defines other inputs which are used to collect data fields like text, dates, choices etc.

post-redirect-get pattern

One of the common accepted pattern to build forms is to use post-redirect-get pattern. This saves our web application from duplicate submission of form data in case page is refreshed by end user. Visual representation for post-redirect-get is as follows:

sequenceDiagram
  participant Browser
  participant Server
  Browser->>Server: HTTP GET /create 
  Server--xBrowser: 200 Ok 
  Browser->>Server: HTTP POST /create
  Note right of Server: save data to db
  Server--xBrowser: 303 See Other /survey/1
  Browser->>Server: HTTP GET /survey/1
  Server--xBrowser: 200 Ok 
  Note left of Browser: renders saved data
  1. Browser first fires HTTP GET request to web server which returns form to be filled by user.
  2. Form has HTTP POST method. When form is filled and submitted, this POST endpoint will be called.
  3. Server will read the posted form, it will save the data to persistent storage.
  4. Once data is saved, Server will send 303 See Other status code.
  5. 303 redirect code instructs browser to presume that the server has received the data and should issue a new GET request to the given URI /survey/1.
  6. Browser issues HTTP GET request to /survey/1 which should render data filled by user.
  7. In case user now refreshes page, it won’t re-post data, instead it will fire HTTP GET to redirected url /survey/1.

rendering form to browser

For getting html output we have to make few changes to the web server code

  1. Before returning html, we have to set Content type to text/html in request header.
1
w.Header().Set("Content-type", "text/html")
  1. Our form will be separate .html file or .gohtml file. We are required to parse this html content and return it. These are called html template files. To work with html template files, Go provides html/template package.
  2. There are two steps for working with html template
    1. Use template.ParseFiles to parse html file. It returns *Template type.
    2. On this *Template type, we call Execute which has following signature
    1
    2
    3
    4
    5
    6
    
     func (t *Template) Execute(wr io.Writer, data interface{}) error {
       if err := t.escape(); err != nil {
         return err
       }
       return t.text.Execute(wr, data)
     }
    

    3.Execute accepts io.writer interface where template will be written and data which will be inserted into template. Currently we are not inserting any data to template and we will pass nil.

Lets spin a server to return survey form.

app.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
  "fmt"
  "html/template"
  "net/http"
)

func handleForm(w http.ResponseWriter, r *http.Request) {
  file := "form.html"
  res, err := template.ParseFiles(file)
  if err != nil {
      w.WriteHeader(http.StatusInternalServerError)
      return
  }
  w.Header().Set("Content-type", "text/html")
  err = res.Execute(w, nil)
  if err != nil {
      w.WriteHeader(http.StatusInternalServerError)
      return
  }
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", handleForm)
  http.ListenAndServe(":3000", mux)
}

form.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<form action="/create" method="POST">
  <h2>Survey Form</h2>
  <label for="firstName">Enter First Name</label>
  <input type="text" required="required" name="firstName" id="firstName">
  <br>
  <br>
  <label for="feedback">Feedback</label>
  <textarea name="feedback" id="feedback" required="required" ></textarea>
  <br>
  <br>
  <input type="submit" value="Submit">
</form>

browser

get form

accept the posted form

Once user has filled form in browser, we need to provide a way for user to send data back to server.

  1. This is achieved by using action and method on form.
    1
    
    <form action="/create" method="POST">
    
  2. When user submits form, this code will POST the form to /create endpoint.
  3. We need to register new route handler that will listen to /create endpoint.
    1
    
     mux.HandleFunc("/create", createSurvey)
    
  4. And create createSurvey to handle incoming request. Let’s just create the handler and send back the form that was posted by user.
    1
    2
    3
    4
    
     func createSurvey(w http.ResponseWriter, r *http.Request) {
       r.ParseForm()
       fmt.Fprintln(w, r.PostForm)
     }   
    
  5. This is how ParseForm works. It parses posted form as well as raw query from the url.
    1
    2
    3
    4
    5
    6
    7
    8
    
     // ParseForm populates r.Form and r.PostForm.
     //
     // For all requests, ParseForm parses the raw query from the URL and updates
     // r.Form.
     //
     // For POST, PUT, and PATCH requests, it also parses the request body as a form
     // and puts the results into both r.PostForm and r.Form. Request body parameters
     // take precedence over URL query string values in r.Form.
    
  6. If you send request from browser now, you should see the posted form displayed as a map

route parameters

We want a single request handler that can return survey based on id passed as route parameter. Following requests should be handled by a single handler
HTTP GET /survey/1 => Return survey with id 1
HTTP GET /survey/2 => Return survey with id 2

This is not possible with NewServerMux() in standard Go library. We will use third party router github.com/julienschmidt/httprouter for this.

To get this package, simple run
go get github.com/julienschmidt/httprouter

save form data and show created survey

To simplify this example, let’s create an InMemory storage instead of real database where we will be saving the form data. Important thing to note that this should be a shared storage which is accessible to all the handlers. We will refactor and define our handlers over a struct which can provide convenience methods to read and write to InMemory storage.

  1. First define a new type for storing survey

    1
    2
    3
    4
    5
    
     type survey struct {
       id       int
       name     string
       feedback string
     }
    
  2. Define our struct that will store all the survey responses in memory.

    1
    2
    3
    
     type formHandler struct {
       surveys []survey
     }
    
  3. Define convenience method to add new survey

    This code is not safe against concurrent access. Do not use in production

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
     func (f *formHandler) addFeedback(name, feedback string) int {
       total := len(f.surveys)
       id := total + 1
       newSurvey := survey{
         id:       id,
         name:     name,
         feedback: feedback,
       }
       f.surveys = append(f.surveys, newSurvey)
       return id
     }
    
    
  4. Define convenience method to get a survey by id

    1
    2
    3
    4
    5
    6
    7
    8
    
     func (f *formHandler) getFeedback(id int) (survey, error) {
       for _, s := range f.surveys {
         if s.id == id {
           return s, nil
         }
       }
       return survey{}, errors.New("survey not found")
     }
    
  5. Define routing

    1
    2
    3
    4
    5
    
     router := httprouter.New()
     router.GET("/", forms.handleForm)
     router.POST("/create/survey", forms.createSurvey)
     router.GET("/survey/:id", forms.showSurvey)
    
    

Complete web app should look like

app.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
  "net/http"
  
  "github.com/julienschmidt/httprouter"
)

func main() {
  forms := &formHandler{
      surveys: []survey{},
  }
  router := httprouter.New()
  router.GET("/", forms.handleForm)
  router.POST("/create/survey", forms.createSurvey)
  router.GET("/survey/:id", forms.showSurvey)

  http.ListenAndServe(":3000", router)
}

types.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "errors"

type survey struct {
  id       int
  name     string
  feedback string
}
type formHandler struct {
  surveys []survey
}

func (f *formHandler) addFeedback(name, feedback string) int {
  total := len(f.surveys)
  id := total + 1
  newSurvey := survey{
      id:       id,
      name:     name,
      feedback: feedback,
  }
  f.surveys = append(f.surveys, newSurvey)
  return id
}

func (f *formHandler) getFeedback(id int) (survey, error) {
  for _, s := range f.surveys {
      if s.id == id {
          return s, nil
      }
  }
  return survey{}, errors.New("survey not found")
}

handlers.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
  "fmt"
  "html/template"
  "net/http"
  "strconv"

  "github.com/julienschmidt/httprouter"
)

func (f *formHandler) handleForm(w http.ResponseWriter, r *http.Request,
  p httprouter.Params) {
  file := "form.html"
  res, err := template.ParseFiles(file)
  if err != nil {
      w.WriteHeader(http.StatusInternalServerError)
      return
  }
  w.Header().Set("Content-type", "text/html")
  err = res.Execute(w, nil)
  if err != nil {
      w.WriteHeader(http.StatusInternalServerError)
      return
  }
}

func (f *formHandler) createSurvey(w http.ResponseWriter, r *http.Request,
  p httprouter.Params) {
  r.ParseForm()
  name := r.PostForm.Get("firstName")
  feedback := r.PostForm.Get("feedback")
  id := f.addFeedback(name, feedback)

  http.Redirect(w, r, fmt.Sprintf("/survey/%d", id), http.StatusSeeOther)
}
func (f *formHandler) showSurvey(w http.ResponseWriter, r *http.Request,
  p httprouter.Params) {
  id, err := strconv.Atoi(p.ByName("id"))
  if err != nil {
      w.WriteHeader(http.StatusInternalServerError)
  }
  survey, err := f.getFeedback(id)
  if err != nil {
      w.WriteHeader(http.StatusNotFound)
  }
  fmt.Fprintf(w, "Survey with id= %d created successfully\n", survey.id)
  fmt.Fprintf(w, "username=%s, feedback=%s", survey.name, survey.feedback)

}

form.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<form action="/create/survey" method="POST">
  <h2>Survey Form</h2>
  <label for="firstName">Enter First Name</label>
  <input type="text" required="required" name="firstName" id="firstName">
  <br>
  <br>
  <label for="feedback">Feedback</label>
  <textarea name="feedback" id="feedback" required="required" ></textarea>
  <br>
  <br>
  <input type="submit" value="Submit">
</form>

browser

First request

Submit

get all surveys

Let’s build a simple endpoint to return all saved surveys

  1. Create a route registration for getting all surveys
    1
    
    router.GET("/surveys", forms.allSurveys)
    
  2. Define allSurvey Handler
1
2
3
4
 func (f *formHandler) allSurveys(w http.ResponseWriter, r *http.Request,
   p httprouter.Params) {
   fmt.Fprintln(w, f.surveys)
 }   
  1. Create multiple survey responses and call /survey endpoint
    1
    2
    
    $ curl http://localhost:3000/surveys
    [{1 sumit good food} {2 amit ok food}]
    

This is how we build forms with Go.


Sumit
WRITTEN BY
Sumit
Gopher


What's on this Page