UP | HOME
Land of Lisp

Zhao Wei

How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

Build a RESTful API using Golang(updating)

1. Requirement

Build a RESTful API that can create/read(get)/update/delete/ aka CRUD user data from a persistence database, with the following User model

{
  "id": "as user id",
  "name": "as user name",
  "dob": "as date of birth",
  "address": "as user address",
  "description": "as user description",
  "createdAt": "as user created time"
}

1.1. Functionality

  1. The API should follow typical RESTful API design pattern.
  2. The data should be saved in DB (NoSQL DB is preferred).
  3. Provide proper unit test.
  4. Provide proper API document.
  5. (Optional), user auth using OAuth.
  6. (Optional), a complete logging strategy.
  7. (Optional), user instances need to link to each other. How to design the model and what API you would build for querying or modifying it?
  8. (Optional), supppose the address of user now includes a geographic coordinate(latitude and longitude), can you build an API that
    • given a user name
    • return the nearby friends

2. Golang code structure

zw@DESKTOP-TTOPQ58:manageUser$ tree .
.
├── go.mod
├── go.sum
├── main.go
├── model
│   ├── index.go
│   ├── local.go
│   └── mongodb.go
└── tools
    └── common.go

2 directories, 7 files
  • main.go as controller
  • model folder contains data models one using local data from memory, another is using MongoDB.
  • tools folder contains common useful functions.

3. Process JSON

Hanlding JSON places an important role in our service since it is the format we communicate with clients.

3.1. Encoding and Decoding

In general, we use standard package encoding/json

  • Unmarshal, which decode the json binary into struct.
  • Marshal, which encode struct into json binary. Especially, when write struct back to Http Response, we could do: json.NewEncoder(w).Encode(ResponseMessage{Status: "400", Message: "failed"}) to send our custom struct back directly, where w is http.ResponseWriter.

3.2. Decoding arbitrary data

The interface{} (empty interface) type describes an interface with zero methods. Every Go type implements at least zero methods and therefore satisfies the empty interface. We use it to represent arbitrary data.

  1. Suppose there is a binary data stored in variable b as

    b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
    
  2. Declare a variable f with type interface{} and Unmarshal the binary into it.

    var f interface{}
    err := json.Unmarshal(b, &f)
    
  3. Use type assertion to reveal its underlying structure: a map whose keys are strings and whose values are themselves stored as empty interface values.

    m := f.(map[string]interface{})
    
  4. At last, iterate through the map using a type switch to access its values as their concrete types:

    for k, v := range m {
        switch vv := v.(type) {
        case string:
    	fmt.Println(k, "is string", vv)
        case float64:
    	fmt.Println(k, "is float64", vv)
        case []interface{}:
    	fmt.Println(k, "is an array:")
    	for i, u := range vv {
    	    fmt.Println(i, u)
    	}
        default:
    	fmt.Println(k, "is of a type I don't know how to handle")
        }
    }
    

3.3. Refernece Type

For decoding arbitrary data, we convert the binary into a map whose keys are strings and whose values are themselves stored as empty interface values. In fact, we could directly Unmarshal the binary into an existing struct.

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)
  • Unmarshl will populate the Parents field, change it from nil to a slice value exists in JSON binary.
  • This is typical of how Unmarshal works with the supported reference types (pointers, slices, and maps).

    type Foo struct {
        Bar *Bar
    }
    

    For example, If there were a Bar field in the JSON object, Unmarshal would allocate a new Bar and populate it. If not, Bar would be left as a nil pointer.

  • Therefore, we could nil to test if some attribute exists in from JSON binary if that attribute is one of pointers, slices, and maps.

4. Persistent data using MongoDB

For simple demo, we will use MongoDB Atlas.

  1. Follow through the steps from Get started with MongoDB Atlas.
  2. Click on the cluster you just created –> click tab “Collections” –> click “+ Create Database”
  3. Use the code example from Connect to MongoDB Atlas to test your first connection with your database.
  4. Corresponding MongoDB types could be viewed at mongo-driver.

5. Useful Notes

5.1. cURL

  1. Get all Users

    curl http://localhost:10000/users -i
    
  2. Create a single User using POST request with json format

    curl -i -X POST -H 'Content-Type: application/json' -d '{"id":"test01", "description":"test"}' http://localhost:10000/user
    
  3. Get a single User by its id

    curl http://localhost:10000/user/test01 -i
    
  4. Delete a single user by its id using DELETE request

    curl -i -X DELETE http://localhost:10000/user/test01
    

5.2. Golang proxy

When install MongoDB Go driver, it is not accessible from China, so to install it, we need to set Golang proxy

  1. Set proxy according to your location

    go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
    # Or other proxy
    go env -w GOPROXY=https://goproxy.io/zh/,direct
    
  2. Test proxy setting works

    time go get golang.org/x/tour
    
  3. To unset proxy

    go env -u GOPROXY
    
    # Now it should show default one: 
    go env | grep proxy
    # GOPROXY="https://proxy.golang.org,direct"
    

6. References