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/http
Build 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))
:HandleFunc
Register a handler function for the given pattern.func ListenAndServe(addr string, handler Handler) error
:ListenAndServe
Listen 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/json
Say 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.