《Building RESTful Web services with GO》 涉及面比较广,但是讲得太浅

1. Getting started with REST API Development

Live reloading the application with supervisord and gulp

2. Handling Routing for Rest Srevices

ServeMux a basic router in Go

go get github.com/julienschmidt/httprouter

// execService.go

// Package main  provides ...
package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "os/exec"

    "github.com/julienschmidt/httprouter"
)

func getCommandOutput(command string, arguments ...string) string {
    cmd := exec.Command(command, arguments...) //unpack
    var out bytes.Buffer
    var stderr bytes.Buffer
    cmd.Stdout = &out
    cmd.Stderr = &stderr
    err := cmd.Start()
    if err != nil {
        log.Fatal(fmt.Sprint(err) + ": " + stderr.String())
    }
    err = cmd.Wait()
    if err != nil {
        log.Fatal(fmt.Sprint(err) + ": " + stderr.String())
    }
    return out.String()
}

func goVersion(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
    fmt.Fprintf(w, getCommandOutput("/usr/local/bin/go", "version"))
}

func getFileContent(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
    fmt.Fprintf(w, getCommandOutput("/bin/cat", params.ByName("name")))
}

func main() {
    router := httprouter.New()
    router.GET("/api/v1/go-version", goVersion)
    router.GET("/api/v1/show-file/:name", getFileContent)
    log.Fatal(http.ListenAndServe(":8000", router))
}

Building the simple static file server

package main

import (
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

func main() {
    router := httprouter.New()
    router.ServeFiles("/static/*filepath", http.Dir("/users/naren/static"))
    log.Fatal(http.ListenAndServe(":8000", router))
}

Gorilla Mux, a powerful HTTP router

  • Path-based matching
  • Query-based matching
  • Domain-based matching
  • Sub-domain based matching
  • Reverse URL generation

3. Working with Middleware and RPC

What is middleware?

When a piece of code needs to be executed for every request or subset of HTTP requests.

Creating a basic middleware

// custommiddleware.go
package main

func middleware(handler http.handler) http.handler {
    return http.handlerfunc(func(w http.responsewriter, r *http.request) {
        fmt.println("executing middleware before request phase!")
        //pass control back to handler
        handler.servehttp(w, r)
        fmt.println("executing middleware after response phase!")
    })
}

func mainlogic(w http.responsewriter, r *http.request) {
    // business logic here
    fmt.println("executing mainhandler")
    w.write([]byte("ok"))
}

func main() {
    manlogichandler := http.handlerfunc(mainlogic)
    http.handler("/", middleware(manlogichandler))
    http.listenandserve(":8000", nil)
}

Multiple middleware and chaining

// cityAPI.go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type city struct {
    Name string
    Area uint64
}

func mainLogic(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        var tempCity city
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&tempCity)
        if err != nil {
            panic(err)
        }
        defer r.Body.Close()
        // creat logic
        fmt.Printf("%s %d", tempCity.Name, tempCity.Area)
        // tell everything is fine
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("201 - Created"))
    } else {
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte("405 - Method Not Allowed"))
    }
}

func main() {
    http.HandleFunc("/city", mainLogic)
    http.ListenAndServe(":8000", nil)
}

抽象出来俩middleware:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "time"
)

type city struct {
    name string
    area uint64
}

// middleware to check content type as json
func filtercontenttype(handler http.handler) http.handler {
    return http.handlerfunc(func(w http.responsewriter, r *http.request) {
        log.println("currently in the check content type middleware")
        // filtering request by mime type
        if r.header.get("content-type") != "application/json" {
            w.writeheader(http.statusunsupportedmediatype)
            w.write([]byte("415 - unsupported media type. please send json"))
            return
        }
        handler.servehttp(w, r)
    })
}

// middle to add server timestamp for response cookie
func setservertimecookie(handler http.handler) http.handler {
    return http.handlerfunc(func(w http.responsewriter, r *http.request) {
        handler.servehttp(w, r)
        cookie := http.cookie{name: "server-time(utc)", value: strconv.formatint(time.now().unix(), 10)}
        http.setcookie(w, &cookie)
        log.println("currently in the set server time middleware")
    })
}

func mainlogic(w http.responsewriter, r *http.request) {
    if r.method == "post" {
        var tempcity city
        decoder := json.newdecoder(r.body)
        err := decoder.decode(&tempcity)
        if err != nil {
            panic(err)
        }
        defer r.body.close()
        // creat logic
        fmt.printf("%s %d", tempcity.name, tempcity.area)
        // tell everything is fine
        w.writeheader(http.statusok)
        w.write([]byte("201 - created"))
    } else {
        w.writeheader(http.statusmethodnotallowed)
        w.write([]byte("405 - method not allowed"))
    }
}

func main() {
    mainlogichnadler := http.handlerfunc(mainlogic)
    http.handler("/city", filtercontenttype(setservertimecookie(mainlogichnadler)))
    http.listenandserve(":8000", nil)
}

Paniless middleware chaining with Alice

//go get github.com/justinas/alice

func main() {
    mainLogicHandler := http.HandlerFunc(mainLogic)
    chain := alice.New(filterContentType, setServerTimeCookie).Then(mainLogicHandler)
    http.Handle("/city", chain)
    http.ListenAndServe(":8000", nil)
}

Using Gorilla's Handlers middleware for Logging

  • LoggingHandler
  • CompressingHandler: for zipping the responses
  • RecoveryHandler: for recovering from unexpected panics

What is RPC?

"net/rpc"

JSON RPC using Gorilla RPC

4. Simplifying RESTful Services with Popular Go Frameworks

go-restful, a framework for REST API creation

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/emicklei/go-restful"
)

// brew install sqlite3
// go get github.com/emicklei/go-restful
// a simple ping server echoes the server time back to the client
func main() {
    webservice := new(restful.WebService)
    webservice.Route(webservice.GET("/ping").To(pingTime))
    restful.Add(webservice)
    http.ListenAndServe(":8000", nil)
}

func pingTime(req *restful.Request, resp *restful.Response) {
    io.WriteString(resp, fmt.Sprintf("%s", time.Now()))
}

// curl -X GET "http://localhost:8000/ping"

CRUD and SQLite3 basics

// sqliteFundamentals.go
package main

import (
    "database/sql"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

// go get github.com/mattn/go-sqlite3

type Book struct {
    id     int
    name   string
    author string
}

func main() {
    db, err := sql.Open("sqlite3", "./books.db")
    log.Println(db)
    if err != nil {
        log.Println(err)
    }
    //create table
    statement, err := db.Prepare("create table if not exists books(id integer primary key, isbn integer, author(varchar(64), name varchar(64) NULL)")
    if err != nil {
        log.Println("Error in creating table")
    } else {
        log.Println("success created table books")
    }
    statement.Exec()
    // Create
    statement, _ = db.Prepare("INSERT INTO books (name, author, isbn) VALUES (?, ?, ?)")
    statement.Exec("A Tale of Two Cities", "Charles Dickens", 140430547)
    log.Println("Inserted the book into database!")
    // Read
    rows, _ := db.Query("SELECT id, name, author FROM books")
    var tempBook Book
    for rows.Next() {
        rows.Scan(&tempBook.id, &tempBook.name, &tempBook.author)
        log.Printf("ID:%d, Book:%s, Author:%s\n", tempBook.id,
            tempBook.name, tempBook.author)
    }
    // Update
    statement, _ = db.Prepare("update books set name=? where id=?")
    statement.Exec("The Tale of Two Cities", 1)
    log.Println("Successfully updated the book in database!")
    //Delete
    statement, _ = db.Prepare("delete from books where id=?")
    statement.Exec(1)
    log.Println("Successfully deleted the book in database!")
}

// 略坑啊:
// This program runs on Windows and Linux without any problem. In Go versions less than 1.8.1, you may see problems on macOS X such as Signal Killed. This is because of the Xcode version; please keep this in mind.

Building a Metro Rail APi with go-restful

// go get github.com/emicklei/go-restful

Building RESTful APIs with the Gin framework

// go get github.com/gin-gonic/gin
// ginBasic.go
package main

import (
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/pingTime", func(c *gin.Context) {
        c.JSON(200, gin.H{"serverTime": time.Now().UTC()})
    })
    r.Run(":8000")
}

Building a RESTful API with Revel.go

Like Python's Django.

go get github.com/revel/revel

5. Working with MongoDB and Go to Create REST APIs

Introducig mgo, a MongoDB deiver for Go

get get gopkg.in/mgo.v2

6. Wokring with Protocol Bufffers and GRPC

Protocol buffer language

  • Scalar values, Enumerations, Defalut values, Nested values, Unknown types
syntax 'proto3';

message NetworkInterface {
  int index=1;
  int mtu=2;
  string name=3;
  string hardwareaddr=4;
}

the protocol buffer message will be converted to a Go struct and fiels are filled with empty defaut values.

Compiling a protocol buffer with protoc

install protobuf compiler on your meachine. brew install protobuf

GRPC

go get google.golang.org/grpc

go get -u github.com/golang/protobuf/protoc-gen-go

Grpc has the following benefits over HTTP/REST/JSON architecture:

  • GPRC use http/2, which is a binary protocol
  • Header compression is possible in http2
  • we can multiplex many requests on one connection
  • Usage of protobufs for strict typing of data
  • streaming of requests or responses is possible instead of request/response transactions

Bidirectional streaming with GRPC

7. Working with PostgreSQL, JSON, and Go

install PostgreSQL

brew install postgresql && brew services start postgresql // port 5432 // use psql shell create new use and database and

pq, a pure PostgreSQL database driver for Go

go get github.com/lib/pq

Exploring thj JSON store in PostgreSQL

PostgreSQL > 9.2 has a feature called the JSON store.

GORM, a powerful ORM for GO

go get -u github.com/jinzhu/gorm

8. Building a REST API Client in Go and Unit Testing

Basics for writing a command-line tool in Go

// flagExample.go
package main

import (
    "flag"
    "log"
)

var name = flag.String("name", "stranger", "you wonderfule name")

func main() {
    flag.Parse()
    log.Printf("Hello %s", *name)
}
// ./flagExample -name laowang or ./flagExample -name=laowang

CLI - a library for building beautiful clients

go get github.com/urfave/cli

// cliBasci.go
package main

import (
    "log"
    "os"

    "github.com/urfave/cli"
)

func main() {
    app := cli.NewApp()
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:  "name",
            Value: "stranger",
            Usage: "you wonderful name",
        },
        cli.IntFlag{
            Name:  "age",
            Value: 0,
            Usage: "your graceful age",
        },
    }
    app.Action = func(c *cli.Context) error {
        log.Printf("Hello %s (%d years), Welcome to the command line world", c.String("name"), c.Int("age"))
        return nil
    }
    app.Run(os.Args)
}

Collecting command-line arguments in CLI

// storeMarks.go
package main

import (
    "log"
    "os"

    "github.com/urfave/cli"
)

func main() {
    app := cli.NewApp()
    // define flags
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:  "save",
            Value: "no",
            Usage: "Should save to database (yes/no)",
        },
    }

    app.Version = "1.0"
    // define action
    app.Action = func(c *cli.Context) error {
        var args []string
        if c.NArg() > 0 {
            // Fetch arguments in a array
            args = c.Args()
            personName := args[0]
            marks := args[1:len(args)]
            log.Println("Person: ", personName)
            log.Println("marks", marks)
        }
        // check the flag value
        if c.String("save") == "no" {
            log.Println("Skipping saving to the database")
        } else {
            // Add database logic here
            log.Println("Saving to the database", args)
        }
        return nil
    }

    app.Run(os.Args)
}

// ./storeMarks --save=yes Albert 89 85 97

grequests - a REST API package for GO

go get -u github.com/levigross/grequests

package main

import (
    "log"

    "github.com/levigross/grequests"
)

func main() {
    resp, err := grequests.GET("http://httpbin.org/get", nil)
    if err != nil {
        log.Fatalln("Unabled to make requests: ", err)
    }
    log.Println(resp.String())
}
package main

import (
    "log"

    "github.com/levigross/grequests"
)

func main() {
    resp, err := grequests.GET("http://httpbin.org/get", nil)
    if err != nil {
        log.Fatalln("Unabled to make requests: ", err)
    }
    log.Println(resp.String())
}
/*
type Response struct {
    Ok bool
    Error error
    RawResponse *http.Response
    StatusCode int
    Header http.Header
}
*/

Getting comfortable with the GitHub REST API

// https://developer.github.com/v3/

// export GITHUB_TOKEN=YOUR_GITHUB_ACCESS_TOKEN
// getRepos.go
package main

import (
  "github.com/levigross/grequests"
  "log"
  "os"
)

var GITHUB_TOKEN = os.Getenv("GITHUB_TOKEN")
var requestOptions = &grequests.RequestOptions{Auth: []string{GITHUB_TOKEN, "x-oauth-basic"}}

type Repo struct {
  ID int `json:"id"`
  Name string `json:"name"`
  FullName string  `json:"full_name"`
  Forks int `json:"forks"`
  Private bool `json:"private"`
}

func getStats(url string) *grequests.Response{
  resp, err := grequests.Get(url, requestOptions)
  // You can modify the request by passing an optional RequestOptions struct
  if err != nil {
    log.Fatalln("Unable to make request: ", err)
  }
  return resp
}

func main() {
  var repos []Repo
  var repoUrl = "https://api.github.com/users/torvalds/repos"
  resp := getStats(repoUrl)
  resp.JSON(&repos)
  log.Println(repos)
}

Using Redis for caching the API data

go get github.com/go-redis/redis

Creating a unit testing tool for our URL shortening service

package main_test

import (
    "net/http"
    "testing"
)

func TestGetoOriginalURL(t *testing.T) {
    response, err := http.Get("http://localhost:8000/v1/short/1")
    if http.StatusOK != response.StatusCode {
        t.Errorf("Expected response code %d. Got %d\n", http.StatusOK)
    }
    if err != nil {
        t.Errorf("encoutered an error:", err)
    }
}

9. Scaling Our REST API Using Microservices

The disadvantage list of a monolithic application could be:

  • Tightly coupled architecture
  • Single point of failure
  • Velocity of adding new features and components
  • Fragmentation of work is limited to teams
  • Continuous deployment is very tough because an entire application needs to be pushed

Microservices bring the following benefits to the plate:

  • If the team is big, people can work on chunks of applications
  • Adaptability is easy for the new developers
  • Adopting best practices, such as Continuous Integration (CI) and Continuous Delivery (CD)
  • Easily replaceable software with loosely coupled architecture

Go Kit, a package for building microservices

go get github.com/go-kit/kit

10. Deplying Our REST srevices

proxy server

The benefits of having a proxy server (Nginx):

  • It can act as a load balancer
  • It can sit in front of cluster of applications and redirect HTTP requests
  • It can serve a filesystem with a good performance
  • It streams media very well

Load balancing with Nginx

  • Round Robin
  • Least Coonection: send to the server that is currently serving the least number of clients
  • IP HASH
  • Least Time: send to the machine with the lowest average latency time

Rate limiting our REST API

limit_conn_zone

11. Using an API Gateway to Monitor and Metricize REST API

They are authentication, logging, rate limiting, and so on. The best way to add those features is to use an API gateway.

Why is an API gateway required?

An api gateway is a fine piece of middleware does these things:

  • Loggig
  • Security
  • Traffic control
  • Transformations

Kong, an open-source API gateway

Kong is a cloud-native, fast, scalable, and distributed Microservice Abstraction Layer (also known as an API Gateway, API Middleware or in some cases Service Mesh). Made available as an open-source project in 2015, its core values are high performance and extensibility. Combination of OpenResty and Nginx.

12 Handling Authentication for Our REST Services

  • session-based authentication
  • JSON Web Tokens (JWT)
  • Authentication2(OAuth2)
//go get github.com/gorilla/sessions
var store = sessions.NewCookieStore([]byte("secret_key"))

session-based authentication

package main

import (
    "log"
    "net/http"
    "os"
    "time"

    "github.com/gorilla/mux"
    "github.com/gorilla/sessions"
)

// os.Getenv return empty string "" if not exists key
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))

var users = map[string]string{"naren": "passme", "admin": "password"}

func HealthcheckHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    if (session.Values["authenticated"] != nil) && session.Values["authenticated"] != false {
        w.Write([]byte(time.Now().String()))
    } else {
        http.Error(w, "Forbidden", http.StatusForbidden)
    }
}

func LoginHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "pass the data as URL form encoded", http.StatusBadRequest)
        return
    }
    username := r.PostForm.Get("username")
    password := r.PostForm.Get("password")

    if originalPassword, ok := users[username]; ok {
        if password == originalPassword {
            session.Values["authenticated"] = true
            session.Save(r, w) // save to response
        } else {
            http.Error(w, "Invalid credentials", http.StatusUnauthorized)
            return
        }
    } else {
        http.Error(w, "user is not found", http.StatusNotFound)
        return
    }
    w.Write([]byte("Logged In success"))
}

func LogoutHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    session.Values["authenticated"] = false
    session.Save(r, w)
    w.Write([]byte(""))
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/login", LogoutHandler)
    r.HandleFunc("/health", HealthcheckHandler)
    r.HandleFunc("/logout", LogoutHandler)
    http.Handle("/", r)
    srv := &http.Server{
        Handler:      r,
        Addr:         "127.0.0.1:8000",
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())

}
# test py code
import requests

resp = requests.post("http://localhost:8000/login", data={"username": "admin", "password": "password"})
print(resp.text)
assert resp.status_code == 200

cookies = resp.cookies
resp = requests.get("http://localhost:8000/health", cookies=cookies)
print(resp.text)
assert resp.status_code == 200

JWT

Json web token format

  • Create a JWT header by doing Base64Url encoding on the header JSON.
  • Create a JWT payload by doing Base64Url encoding on the payload JSON.
  • Create a signature by encrypting the appended header and payload using a secret key.
  • JWT string can be obtained by appending the header, payload, and signature.

signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) tokenString = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature

go get github.com/dgrijalva/jwt-go

Auth2

Authentication is the process of identifying whether a client is genuine or not. When a server authenticates a client, it checks the username/password pair and creates session cookie/JWT.

Authorization is the process of differentiating one client from another after a successful authentication. In cloud services, the resources requested by a client need to be served by checking that the resources belong to that client but not the other client. The permissions and access to resources vary for different clients. For example, the admin has the highest privileges of resources. A normal user's access is limited.

OAuth2 is a protocol for authenticating multiple clients to a service, whereas the JWT is a token format. We need to encode/decode JWT tokens to implement the second stage (dashed lines in the following screenshot) of OAuth 2.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    jwt "github.com/dgrijalva/jwt-go"
    "github.com/dgrijalva/jwt-go/request"
    "github.com/gorilla/mux"
)

var secretKey = []byte(os.Getenv("SESSION_SECRET"))

// var secretKey = []byte("test")

var users = map[string]string{"naren": "passme", "admin": "password"}

// json response for jwt
type Response struct {
    Token  string `json:"token"`
    Status string `json:"status"`
}

func HealthcheckHandler(w http.ResponseWriter, r *http.Request) {
    tokenString, err := request.HeaderExtractor{"access_token"}.ExtractToken(r)
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        // hmacSampleSecret is a
        return secretKey, nil
    })
    if err != nil {
        w.WriteHeader(http.StatusForbidden)
        w.Write([]byte("Access Denied; Please check access token"))
        return
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        // If token is valid
        response := make(map[string]string)
        // response["user"] = claims["username"]
        response["time"] = time.Now().String()
        response["user"] = claims["username"].(string) // claims value is interface, convert to string
        responseJSON, _ := json.Marshal(response)
        w.Write(responseJSON)
    } else {
        w.WriteHeader(http.StatusForbidden)
        w.Write([]byte(err.Error()))
    }
}

// LoginHandler validates the user credentials
func getTokenHandler(w http.ResponseWriter, r *http.Request) {
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "Please pass the data as URL form encoded", http.StatusBadRequest)
        return
    }
    username := r.PostForm.Get("username")
    password := r.PostForm.Get("password")
    if originalPassword, ok := users[username]; ok {
        if password == originalPassword {
            // Create a claims map
            claims := jwt.MapClaims{
                "username":  username,
                "ExpiresAt": 15000,
                "IssuedAt":  time.Now().Unix(),
            }
            token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
            tokenString, err := token.SignedString(secretKey)
            if err != nil {
                w.WriteHeader(http.StatusBadGateway)
                w.Write([]byte(err.Error()))
            }
            response := Response{Token: tokenString, Status: "success"}
            responseJSON, _ := json.Marshal(response)
            w.WriteHeader(http.StatusOK)
            w.Header().Set("Content-Type", "application/json")
            w.Write(responseJSON)
        } else {
            http.Error(w, "Invalid Credentials", http.StatusUnauthorized)
            return
        }
    } else {
        http.Error(w, "User is not found", http.StatusNotFound)
        return

    }
}
func main() {
    r := mux.NewRouter()
    r.HandleFunc("/getToken", getTokenHandler)
    r.HandleFunc("/healthcheck", HealthcheckHandler)
    http.Handle("/", r)
    srv := &http.Server{
        Handler: r,
        Addr:    "127.0.0.1:8000",
        // Good practice: enforce timeouts for servers you create!
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}