GO Web Services
Handling Requests
These are the two main ways to handle requests
- Handle a handler to handle requests matching a pattern
- HandleFunc a function to handle requests matching a pattern
Handle
Here is the signature for Handle.
func Handle(pattern string, handler Handler)
Handler is an interface which has the signature
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
So here is how to use this approach. We create a type and implement the interface required.
// Creating a handler type
type fooHandler struct {
Message string
}
// Implementing the function for the interface
func (f *fooHandler) ServeHTTP(w http.ResponseWrite, r *http.Request) {
w.Write( []byte(f.Message) )
}
func main() {
http.Handle("/foo", &fooHandler{Message: "hello Word"})
}
Remember Remember we can construct a struct using a struct literal e.g.
type Student struct {
Name string
Age int
}
pb := &Student{ // pb == &Student{"Bob", 8}
Name: "Bob",
Age: 8,
}
Requests
These consist of three parts
- Method
- Headers
- Body
HandleFunc
Perhaps somewhat simpler. We just need to create a function with the correct signature
func main() {
foo := funct(w http.ResponseWritter, _ *http.Response) {
w.Write( []byte("hello world") )
}
http.HandleFunc("/foo", foo)
}
Handling JSON
Marshal and Unmarshal
Every GO type implements the empty interface so any struct can be marshaled into JSON.
func Marshal(v interface{}) (byte[]. error)
To ensure the struct is marshaled, all the fields need to be exported. In this example surname would not be marchaled.
type foo struct{
Message string
Age int
surname string
}
json,Marshal(&foo{"4Score",56, "Lincoln"})
To Unmarshal
f := foo{}
json.Unmarshal([]byte(`{"Message":"4Score","Age":56, "Name":"Abe"}`), &f)
Demo Data
Demo Type
Lets make a type to encode
package product
// Product
type Product struct {
ProductID int `json:"productId"`
Manufacturer string `json:"manufacturer"`
Sku string `json:"sku"`
Upc string `json:"upc"`
PricePerUnit string `json:"pricePerUnit"`
QuantityOnHand int `json:"quantityOnHand"`
ProductName string `json:"productName"`
}
Make Some Demo Data
Here is some demo data.
[
{
"productId": 1,
"manufacturer": "Johns-Jenkins",
"sku": "p5z343vdS",
"upc": "939581000000",
"pricePerUnit": "497.45",
"quantityOnHand": 9703,
"productName": "sticky note"
},
{
"productId": 2,
"manufacturer": "Hessel, Schimmel and Feeney",
"sku": "i7v300kmx",
"upc": "740979000000",
"pricePerUnit": "282.29",
"quantityOnHand": 9217,
"productName": "leg warmers"
},
...
Below is a example of how to read the json into a map
func loadProductMap() (map[int]Product, error) {
fileName := "products.json"
_, err := os.Stat(fileName)
if os.IsNotExist(err) {
return nil, fmt.Errorf("file [%s] does not exist", fileName)
}
file, _ := ioutil.ReadFile(fileName)
productList := make([]Product, 0)
err = json.Unmarshal([]byte(file), &productList)
if err != nil {
log.Fatal(err)
}
prodMap := make(map[int]Product)
for i := 0; i < len(productList); i++ {
prodMap[productList[i].ProductID] = productList[i]
}
return prodMap, nil
}
Implementing Method Types
Create Handler and Determine Mathod
We can now provide a GET method within the handler. The handler determines which method by looking at the request.
func handleProduct(w http.ResponseWriter, r *http.Request) {
...
switch r.Method {
// GET
// POST
// etc
}
}
GET Method
We use the Mnmarshal method to encode the json
case http.MethodGet:
productsJSON, err := json.Marshal(productList)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(productsJSON)
POST Method
We use the Unmarshal method to decode the json. In the source they replaced the ReadAll Unmarshal in favour of json.NewDocder.
case http.MethodPost:
// add a new product to the list
var newProduct Product
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = json.Unmarshal(bodyBytes, &newProduct)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err := json.NewDecoder(r.Body).Decode(&product)
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if newProduct.ProductID != 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
newProduct.ProductID = getNextID()
productList = append(productList, newProduct)
w.WriteHeader(http.StatusCreated)
Managing the Routes
Example Routes
With REST we generally have one route for all products and one for a particular product. e.g. GET /products/123. To manage this we greate different handlers.
http.Handle("/products", middlewareHandler(productListHandler))
http.Handle("/products/", middlewareHandler(productItemHandler))
Managing the URL
Here is an example of getting the 123 from the URL. Note we return a 404 if we cannot determine the resource and not a bad request.
func productHandler(w http.ResponseWriter, r *http.Request) {
urlPathSegments := strings.Split(r.URL.Path, "products/")
productID, err := strconv.Atoi(urlPathSegments[len(urlPathSegments)-1])
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusNotFound)
return
}
product, listItemIndex := findProductByID(productID)
if product == nil {
http.Error(w, fmt.Sprintf("no product with id %d", productID), http.StatusNotFound)
return
}
switch r.Method {
case http.MethodGet:
...
Middeware
Explanation
I liked this example provided in GO. Just made sense to me. This is like the interceptors in Angular.
func enableCorsMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do stuff before intended handler here
handler.ServeHttpp(w,r)
// do stuff after intended handler here
})
func intendedFunction(w http.ResponseWriter, r *http.Request) {
// business logic here
}
func main() {
intendedHandler := http.HandlerFunc(intendedFunction)
http.Handle("/foo", enableCorsMiddleware(intendedHandler))
http.ListenAndServe(":5000", nil)
}
Wrapping Handlers
When using middleware we need to convert our handler to http.handlers. To do this we use the http.handlerFunc. E.g.
// Before Middleware
// http.HandleFunc("/products", productsHandler)
// http.HandleFunc("/products/", productHandler)
// http.ListenAndServe(":5000", nil)
func main() {
productListHandler := http.HandlerFunc(productsHandler)
productItemHandler := http.HandlerFunc(productHandler)
http.Handle("/products", middlewareHandler(productListHandler))
http.Handle("/products/", middlewareHandler(productItemHandler))
http.ListenAndServe(":5000", nil)
}
Yet another CORs
Well love diagrams to here are another two. CORs prevents same-site access. Where means different ports, protocols subdomains are not allowed.
Below is a sample function to enable CORs. Do it, do it, don't do it.
func enableCorsMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Authorization, X-CSRF-Token, Accept-Encoding")
handler.ServeHTTP(w, r)
})
}
Persisting Data
Connection to Database
Nothing too difficult here.
package services
import (
"database/sql"
"strconv"
"log"
)
type DatabaseConnection struct {
Db *sql.DB
}
func NewDatabaseConnection(
dbHost string,
dbPort int,
dbName, dbUser, dbPass string) *DatabaseConnection {
var connectionString =
dbUser + ":" + dbPass + "@tcp(" + dbHost + ":" + strconv.Itoa(dbPort) + ")/" +
dbName + "?parseTime=true&autocommit=false"
dbConnection, err := sql.Open("mysql", connectionString)
if err != nil {
log.Fatal("Error connecting to Database. Error is %s", err.Error())
}
}
Doing a SELECT
We create a Select Statement using prepare to do this once as it is expenseive
func CreateSelectStatement(databaseConnection *DatabaseConnection) *sql.Stmt {
selectStatement, err := databaseConnection.Db.Prepare(
"SELECT NEW_CASES, NEW_DEATHS, NEW_RECOVERED, TOTAL_CASES, TOTAL_DEATHS, TOTAL_RECOVERED, RECORD_DATE_TIME FROM COVID_DATA WHERE ALPHA2CODE = ? AND YEAR = ? AND DAY = ?")
if err != nil {
log.Fatal("Error Creating Select Statement. Error was %s", err.Error())
}
return selectStatement
}
Now we can bind the arguments to the statement
func (covidDataTable *CovidDataTable) Select(
alpha2Code string,
year, day int) (bool, *models.CovidData) {
// Get the rows
rows, err := covidDataTable.selectStatement.Query(&alpha2Code, &year, &day)
if err != nil {
log.Fatal("Error occurred selecting covid data. Error was %s", err.Error())
return false, nil
}
...
And then we can iterate over the results and make sure we close the rows if there is an error.
...
defer rows.Close()
ok := rows.Next()
if !ok {
log.Fatal("Error occurred Selecting covid data for Country %s, Year %d, Day.", alpha2Code, year, day)
}
var newConfirmed, newDeaths, newRecovered int
var totalConfirmed, totalDeaths, totalRecovered int
var recordDateTime time.Time
err = rows.Scan(
&newConfirmed, &newDeaths, &newRecovered,
&totalConfirmed, &totalDeaths, &totalRecovered, &recordDateTime)
if err != nil {
log.Fatal("Error occurred selecting covid data. Error was %s", err.Error())
return false, nil
}
...
Doing a INSERT
We create a Insert Statement using prepare to do this once as it is expenseive
func CreateInsertStatement(databaseConnection *DatabaseConnection) *sql.Stmt {
insertStatement, err := databaseConnection.Db.Prepare(
"INSERT INTO COVID_DATA(ALPHA2CODE, YEAR, DAY, NEW_CASES, NEW_DEATHS, NEW_RECOVERED, TOTAL_CASES, TOTAL_DEATHS, TOTAL_RECOVERED, RECORD_DATE_TIME) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
if err != nil {
log.Fatal("Error Creating Insert Statement. Error was %s", err.Error())
}
return insertStatement
}
And now we can to the insert
func (covidDataTable *CovidDataTable) Insert(
tx *sql.Tx,
alpha2Code string,
year, day int,
covidData *models.CovidData) bool {
// Do the insert
result, err := tx.Stmt(covidDataTable.insertStatement).Exec(
&alpha2Code,
&year,
&day,
covidData.NewConfirmed,
covidData.NewDeaths,
covidData.NewRecovered,
covidData.TotalConfirmed,
covidData.TotalDeaths,
covidData.TotalRecovered,
covidData.RecordDateTime)
var myError = covidDataTable.insertStatement.Close()
if myError != nil {
log.Fatal("Yes my son %s", myError.Error())
}
// Check for Error
if err != nil {
log.Fatal("Error occurred inserting covid data. Error Executing statement. Error was %s", err.Error())
}
rows, err := result.RowsAffected()
if err != nil {
logFatal("Error occurred inserting covid data. Error getting row affected. Error was %s", err.Error())
}
// All good
return true
}
Managing Database Resources
Connection Pools
These are the parameters available.
- Connection Max Lifetime - Sets maximum amount of time a connection may be used
- Max Idle Connections - Sets the maxium number in idle connection pool - default is 2
- Max Open Connections - Sets the maximum open connections
Using Contexts
These allow you to perform tasks with timeouts. Useful to ensure resource are no exhausted.
func getProduct(productID int) (*Product, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
row := database.DbConn.QueryRowContext(ctx, `SELECT
productId,
manufacturer,
sku,
upc,
pricePerUnit,
quantityOnHand,
productName
FROM products
WHERE productId = ?`, productID)
product := &Product{}
err := row.Scan(
&product.ProductID,
&product.Manufacturer,
&product.Sku,
&product.Upc,
&product.PricePerUnit,
&product.QuantityOnHand,
&product.ProductName,
)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
log.Println(err)
return nil, err
}
return product, nil
}
File Uploading
To upload files we could
- encode the file to base64 and convert to Json
- multipart/form-data type which uses HTTP form submit
Encode
To decode the string we would need to
str := "SGVsbG8gV29ybGQ="
output, err := base64.StdEncoding.DecodString(str)
if err != nill {
log.Fatal(err)
}
fmt.Printf("%q\n", output)
Using Multipart
Uploading
The ParsMultiPartForm will start using disk if the limit is exceeded.
For the roll left bit
op1 << op2 Shift bits of op1 left by distance op2; fills with zero bits on the right-hand side
5 << 20
0101 << 00000000000000000000
010100000000000000000000 (binary) = 50000 (Bytes)
func uploadFileHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(5 << 20) // 5 Mb
file, handler, err := r.FormFile("receipt")
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusBadRequest)
return
}
defer file.Close()
f, err := os.OpenFile(filepath.Join(ReceiptDirectory, handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
defer f.Close()
io.Copy(f, file)
}
Downloading
And here is an example of Downloading
fileName := urlPathSegments[1:][0]
file, err := os.Open(filepath.Join(ReceiptDirectory, fileName))
defer file.Close()
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
fHeader := make([]byte, 512)
file.Read(fHeader)
fContentType := http.DetectContentType(fHeader)
stat, err := file.Stat()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
fSize := strconv.FormatInt(stat.Size(), 10)
w.Header().Set("Content-Disposition", "attachment; filename="+fileName)
w.Header().Set("Content-Type", fContentType)
w.Header().Set("Content-Length", fSize)
file.Seek(0, 0)
io.Copy(w, file)
WebSockets
Introduction
Previously we used to poll for changes. With WebSockets we can solve this problem.
- Client sends HTTP GET request
- Connection: Upgrade
- Upgrade: Websocket
- Sec-WebSocket-Key: key
- Server Responds with status code "101"
- Switching Protocols
- Upgrade: Websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: key
Creating WebSocket
For GO the package is golang.org/x which means
These packages are part of the Go Project but outside the main Go tree. They are developed under looser compatibility requirements than the Go core. Install them with "go get".
I understand there are alternatives. Googling showed Gorilla to be one of the most popular.
So we are going to use golang. Let create a handler.
package test
import (
"net/http"
"golang.org/x/net/websocket"
)
func SetupRoutes(apiBasePath string) {
http.Handle("/websocket", websocket.Handler(testSocket))
}
The interface is
type Handler func(*Conn)
Now implement the handler.
package test
import (
"fmt"
"log"
"time"
"golang.org/x/net/websocket"
)
type message struct {
Data string `json:"data"`
Type string `json:"type"`
}
func testSocket(ws *websocket.Conn) {
// we can verify that the origin is an allowed origin
fmt.Printf("origin: %s\n", ws.Config().Origin)
done := make(chan struct{})
go func(c *websocket.Conn) {
for {
var msg message
if err := websocket.JSON.Receive(ws, &msg); err != nil {
log.Println(err)
break
}
fmt.Printf("received message %s\n", msg.Data)
}
close(done)
}(ws)
loop:
for {
select {
// End the infinate loop
case <-done:
fmt.Println("connection was closed, lets break out of here")
break loop
// Do Send of Data
default:
// Construct Data
testData := TestData{"FRED1", "FRED2"}
if err := websocket.JSON.Send(ws, testData); err != nil {
log.Println(err)
break
}
fmt.Println("Send some data")
// pause for 10 seconds before sending again
time.Sleep(10 * time.Second)
}
}
fmt.Println("closing the connection")
defer ws.Close()
}
Testing a Websocket
We can use the devtools within chrome
let ws = new WebSocket("ws://localhost/localhost:5000/websocket")
ws.send(JSON.stringify({"data":"test data", "type":"Test"})
Templates
Introduction
This is like pug and esj where you provide data and a markup which has placeholders for the data.
package main
import (
"html/template"
"os"
)
type BlogPost struct {
Title string
Content string
}
func main() {
post := BlogPost{"First Post", "This is the blog post content section"}
tmpl, err := template.New("blog-tmpl").Parse("<h1>{{.Title}}</h1><div><p>{{.Content}}</p></div>")
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, post)
if err != nil {
panic(err)
}
}
Pipelines
This is a way of chaining operations together in a template.
// Pass argument to function .SaySomething(text string)
{{ .SaySomething " Hello "}}
// Same as
{{ " Hello " | .SaySomething }}
// And
{{ " Hello " | .SaySomething | printf "%s %s" " World" }}
Looping
I have not checked the code and just wanted an awareness of the concept. I think I would prefer to format on the client but this could be useful when connecting to other reporting system. No surprises but here is an example. Here is the template
<!doctype html>
<head>
</head>
<body style="height: 100%;" }>
<div style="background: #6a7d87; width: 100%; z-index: 100; top: 0;
left: 0; box-shadow: 0 1px 20px transparentize($second-color, 0.5);">
</div>
<div style="font-size: 1.5rem; font-weight: bold; color: #212529; display: block; font-family: Roboto, 'Helvetica Neue', sans-serif;font-stretch: normal; font-weight: bold; text-align: left; padding: .75rem 1.25rem; background-color: rgba(0,0,0,.03); border-bottom: 1px solid rgba(0,0,0,.125); ">
Product Summary Report
</div>
<table style="width: 100%; height: 100%; margin-top: .5em;">
<tr>
<th>Row</th>
<th>Product Name</th>
<th>Quantity On Hand</th>
</tr>
{{range $index, $element := .}}
{{if mod $index 2}} <tr style="background:#6a7d87;"> {{else}}
<tr> {{end}}
<td>
{{$index}}
</td>
<td>
{{.ProductName}}
</td>
<td>
{{.QuantityOnHand}}
</td>
</tr>
{{end}}
</table>
</body>
</html>
And the code for the template demonstrating looping.
t := template.New("report.gotmpl").Funcs(template.FuncMap{"mod": func(i, x int) bool { return i%x == 0 }})
t, err = t.ParseFiles(path.Join("templates", "report.gotmpl"))
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Disposition", "Attachment")
var tpl bytes.Buffer
err = t.Execute(&tpl, products)
Functions
There are functions just like ejs and pug. The ones listed and here is the link https://pkg.go.dev/text/template#hdr-Functions
*and {{if and true true true}} {{end}}
*or {{if or true false true}} {{end}}
*index {{index . 1}}
*len {{len .}}
*not {{if not false}}
*print, printf, println {{println "hey"}}
Custom Functions
We can write our own function but they must return a single value or value,err.
// Usage
tmpl := "{{range &index, $element := .}}{{if mod $index 2}}{{.}}{{end}}}{{end}}}"
// Declare
fm := template.FuncMap{"mod": func(i,j int) bool {return i%} == 0 }}
// Add to Template
tmpl, _ := template.New("tmplt").Funcs(fm).Parse(tmpl)