Due to business reasons of the company, I started to learn Golang recently. Before THAT, I was writing PHP. In the process of learning Golang, I did not adapt to it because there are certain differences between Golang and dynamic scripting language. This article aims to share how I used Golang to write a RESTful micro-framework GoGym (this project is named because I like to go to the gym haha), from idea to implementation.

How to usenet/httpBuild a simple Web service

Golang provides a concise way to build Web services

package main

import (
    "net/http"
)

func HelloResponse(rw http.ResponseWriter, request *http.Request) {
    fmt.Fprintf(w, "Hello world.")
}

func main() {
    http.HandleFunc("/", HelloResponse)
    http.ListenAndServe(":3000", nil)
}Copy the code

There are two core methods:

  • func HandleFunc(pattern string, handler func(ResponseWriter, *Request)):HandleFuncRegister a handler function for the given pattern.
  • func ListenAndServe(addr string, handler Handler) error:ListenAndServeListen for the given TCP network address, then call the Serve method with a handler to receive the request.

After go Build, execute the compiled file and see hello World on the client side

With Web services, you can set small goals

As a first release, I don’t think you need a complex design. You just need to receive the request from the user, find the handler, execute its logic, and return JSON.

If you have a small goal, how do you achieve it?

1. Design how users register controllers and actions

As FAR as I can see, some frameworks preconfigure GET,POST,PUT and other methods in Controller to receive HTTP requests from GET,POST, and PUT. I think it does have advantages, because the user only needs to implement these methods, but it also has disadvantages at the business level, because there is no way to ensure that the Controller responsible for a page or function only receives one GET request, and if there are two GET requests, then we need to create another Controller, Just implement its GET method. So I borrowed the syntax for Laravel registering controllers and actions from the PHP community: Get(“/”, “IndexController@Index”).

The user only needs to define:

type IndexController struct {
}

func (IndexController *IndexController) Index(//params) (//return values) {
}
Copy the code

Of course, that brings a bit of dynamic scripting language to the framework, and Golang’s Reflect library is definitely in play.

2. Design the Path/Controller/Action container

I used Golang’s map and defined data structures like map[string]map[string]map[string] String

To [“/” : “GET” : [” IndexController “:” GET “], “POST” : [” IndexController “:” POST “]], “/ foo” : [” GET “: [” IndexController” : “foo”]]], for example:

This shows that under “/” there are GET and POST requests corresponding to GET and POST methods in IndexController, and under “/foo” there are GET requests corresponding to foo in IndexController. When the request is accepted, if no method is found, 405 is returned.

3. How to bind a registered set of methods to PATH to receive external requests

And we can see, Func HandleFunc(ResponseWriter, *Request) (ResponseWriter, *Request)) Functionfunc (IndexController *IndexController) Index(//params) (//return values) {} Golang has First Class Functions, so we can do the following:

http.HandleFunc(path, HandleRequest())

func HandleRequest() {
    return func(rw http.ResponseWriter, request *http.Request) {
        // do your logic
    }
}Copy the code

And 4.encoding/jsonSay Hi

When we receive the return value from function, we need to json encode the result, which encoding/ JSON does. I used json.marshal ():

Func Marshal(v interface{}) ([]byte, error): Marshal returns the encoding result of v.

How to use

package main

import (
    "net/url"
    "net/http"
    "github.com/ZhenhangTung/GoGym"
)

type IndexController struct {
}

func (IndexController *IndexController) Index(request map[string]url.Values, headers http.Header) (statusCode int, response interface{}) {
    return 200, map[string]string{"hello": "world"}
}

type BarController struct {
}

func (*BarController) Bar(request map[string]url.Values, headers http.Header) (statusCode int, response interface{}, responseHeader http.Header) {
    return 200, map[string]string{"GoTo": "Bar"}, http.Header{"Foo": {"Bar", "Baz"}}
}

func main() {
    var apiService = GoGym.Prepare()
    apiService.Get("index", "IndexController@Index")
    apiService.Post("bar", "BarController@Bar")
    controllers := []interface{}{&IndexController{}}
    apiService.RegisterControllers(controllers)
    apiService.RegisterController(&BarController{})
    apiService.Serve(3000)
}Copy the code

Complete project code

package GoGym

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "reflect"
    "strings"
)

const (
    GETMethod     = "GET"
    POSTMethod    = "POST"
    PUTMethod     = "PUT"
    PATCHMethod   = "PATCH"
    DELETEMethod  = "DELETE"
    OPTIONSMethod = "OPTIONS"
)

const (
    HTTPMethodNotAllowed = 405
)

// APIService for now is the struct for containing controllerRegistry and registeredPathAndController,
// and it is the core service provider
type APIService struct {
    // controllerRegistry is where all registered controllers exist
    controllerRegistry map[string]interface{}
    //registeredPathAndController is a mapping of paths and controllers
    registeredPathAndController map[string]map[string]map[string]string
    requestForm                 map[string]url.Values
}

func (api *APIService) Get(path, controllerWithActionString string) {
    mapping := api.mappingRequestMethodWithControllerAndActions(GETMethod, path, controllerWithActionString)
    api.registeredPathAndController[path] = mapping
}

func (api *APIService) Post(path, controllerWithActionString string) {
    mapping := api.mappingRequestMethodWithControllerAndActions(POSTMethod, path, controllerWithActionString)
    api.registeredPathAndController[path] = mapping
}

func (api *APIService) Put(path, controllerWithActionString string) {
    mapping := api.mappingRequestMethodWithControllerAndActions(PUTMethod, path, controllerWithActionString)
    api.registeredPathAndController[path] = mapping
}

func (api *APIService) Patch(path, controllerWithActionString string) {
    mapping := api.mappingRequestMethodWithControllerAndActions(PATCHMethod, path, controllerWithActionString)
    api.registeredPathAndController[path] = mapping
}

func (api *APIService) Options(path, controllerWithActionString string) {
    mapping := api.mappingRequestMethodWithControllerAndActions(OPTIONSMethod, path, controllerWithActionString)
    api.registeredPathAndController[path] = mapping
}

func (api *APIService) Delete(path, controllerWithActionString string) {
    mapping := api.mappingRequestMethodWithControllerAndActions(DELETEMethod, path, controllerWithActionString)
    api.registeredPathAndController[path] = mapping
}

// mappingRequestMethodWithControllerAndActions is a function for mapping request method with controllers
// which containing actions
func (api *APIService) mappingRequestMethodWithControllerAndActions(requestMethod, path, controllerWithActionString string) map[string]map[string]string {
    mappingResult := make(map[string]map[string]string)
    if length := len(api.registeredPathAndController[path]); length > 0 {
        mappingResult = api.registeredPathAndController[path]
    }
    controllerAndActionSlice := strings.Split(controllerWithActionString, "@")
    controller := controllerAndActionSlice[0]
    action := controllerAndActionSlice[1]
    controllerAndActionMap := map[string]string{controller: action}
    mappingResult[requestMethod] = controllerAndActionMap
    return mappingResult
}

// HandleRequest is a function to handle http request
func (api *APIService) HandleRequest(controllers map[string]map[string]string) http.HandlerFunc {
    return func(rw http.ResponseWriter, request *http.Request) {
        request.ParseForm()
        method := request.Method
        api.requestForm["query"] = request.Form
        api.requestForm["form"] = request.PostForm
        macthedControllers, ok := controllers[method]
        if !ok {
            rw.WriteHeader(HTTPMethodNotAllowed)
        }
        for k, v := range macthedControllers {
            controllerKey := "*" + k
            controller := api.controllerRegistry[controllerKey]
            in := make([]reflect.Value, 2)
            in[0] = reflect.ValueOf(api.requestForm)
            in[1] = reflect.ValueOf(request.Header)
            returnValues := reflect.ValueOf(controller).MethodByName(v).Call(in)
            statusCode := returnValues[0].Interface()
            intStatusCode := statusCode.(int)
            response := returnValues[1].Interface()
            responseHeaders := http.Header{}
            if len(returnValues) == 3 {
                responseHeaders = returnValues[2].Interface().(http.Header)
            }
            api.JSONResponse(rw, intStatusCode, response, responseHeaders)
        }
    }
}

// RegisterHandleFunc is a function registers a handle function to handle request from path
func (api *APIService) RegisterHandleFunc() {
    for k, v := range api.registeredPathAndController {
        path := k
        if !strings.HasPrefix(k, "/") {
            path = fmt.Sprintf("/%v", k)
        }
        http.HandleFunc(path, api.HandleRequest(v))
    }
}

// RegisterControllers is a function registers a struct of controllers into controllerRegistry
func (api *APIService) RegisterControllers(controllers []interface{}) {
    for _, v := range controllers {
        api.RegisterController(v)
    }
}

// RegisterControllers is a function registers a controller into controllerRegistry
func (api *APIService) RegisterController(controller interface{}) {
    controllerType := getType(controller)
    api.controllerRegistry[controllerType] = controller
}

// getType is a function gets the type of value
func getType(value interface{}) string {
    if t := reflect.TypeOf(value); t.Kind() == reflect.Ptr {
        return "*" + t.Elem().Name()
    } else {
        return t.Name()
    }
}

// Serve is a function
func (api *APIService) Serve(port int) {
    api.RegisterHandleFunc()
    fullPort := fmt.Sprintf(":%d", port)
    http.ListenAndServe(fullPort, nil)
}

// JSONResponse is a function return json response
func (api *APIService) JSONResponse(rw http.ResponseWriter, statusCode int, response interface{}, headers http.Header) {
    for k, v := range headers {
        for _, header := range v {
            rw.Header().Add(k, header)
        }
    }
    rw.Header().Add("Content-Type", "application/json")
    rw.WriteHeader(statusCode)
    rsp, err := json.Marshal(response)
    if err != nil {
        // TODO: logging error
        fmt.Println("JSON err:", err)
    }
    rw.Write(rsp)
}

// Prepare is a fucntion prepare the service and return prepared service to the user
func Prepare() *APIService {
    var apiService = new(APIService)
    apiService.controllerRegistry = make(map[string]interface{})
    apiService.registeredPathAndController = make(map[string]map[string]map[string]string)
    apiService.requestForm = make(map[string]url.Values)
    return apiService
}
Copy the code

Next up

In the next section, I’ll explain how I designed the service and split the code above.


GoGym is still in its early stage, and my understanding of Golang is also limited, so there must be a lot of problems and deficiencies. Welcome students who are interested to go to Github to publish an issue and discuss with us or submit PR.