HTTP

05/02/2020 http client server middleware


All examples from the article are located in the github repository.

Let's take a look to HTTP tools in GO.

Among other standard GO packages there is net/http package, implemented both HTTP-client and -server functionality.

HTTP-server

The simplies way to start HTTP-server — to call http.ListenAndServe:

package main

import (
	"log"
	"net/http"
)

type Handler struct {}

func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request){
	_,err := w.Write([]byte("hello"))
	if err != nil {
		log.Println(err)
	}
}

func main() {
	log.Println(http.ListenAndServe("127.0.0.1:9090", &Handler{}))
}

We just ran server on port 9090.

The second parameter of http.ListenAndServe is http.Handle interface implementer. The interface requires of ServeHTTP(w http.ResponseWriter, req *http.Request) function. This function is going to process all requests on our server:

curl localhost:9090/someurl
hello

curl localhost:9090/secondurl
hello

Also we can configure the server to process the specific URL, for example — /hello:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request){
		_,err := w.Write([]byte("hello"))
		if err != nil {
			log.Println(err)
		}
	})

	log.Println(http.ListenAndServe(":9090", nil))
}

How does it work? When we call http.HandleFunc, the path and handler function is added to net/http.DefaultServeMux struct, that is holding all of them. And when we call particular URL, the net/http.DefaultServeMux is used to find particular handler. If no handler found — standard 404 response is generated.

Standard router implements the only plain URL comparison to each of registered URLs. Pattern matching, such as matching to expression like /hello/{page}, where page parameter can be different between several calls, is impossible with standard router. To implement that we need either to implement pattern matching by ourselves or to use one of router libraries.

Custom router

We could use any other router, for example gorilla/mux. It supports pattern matching.

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/products/{key}", ProductHandler)
	r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
	r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
	http.Handle("/", r)
}

Next, in handler we can grab the variables from the URL:

func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

Middleware

In standard web service there is not only handler's code that is running on every request. Also there is some common code that performs some general work — logging, statistics grabbing, authorization checking, etc. This common code is called middleware.

There no explicit implementation of middlewares in GO standard library. But anyone can implement something like that by using nested handlers. Let's take a look to a logging middleware, that prints every requested URL to a console:



package main

import (
	"log"
	"net/http"
)

type LoggingMiddleware struct {
	handler http.Handler
}

func NewLoggingMiddleware(handler http.Handler) *LoggingMiddleware {
	return &LoggingMiddleware{
		handler: handler,
	}
}

func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	log.Println(req.URL, "requested")

	l.handler.ServeHTTP(w, req)
}

type Handler struct {}

func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request){
	_,err := w.Write([]byte("hello"))
	if err != nil {
		log.Println(err)
	}
}

func main() {
	log.Println(http.ListenAndServe(":9090", NewLoggingMiddleware(&Handler{})))
}

On every request to our server we now can see following data in the console:

2020/05/01 18:09:42 / requested
2020/05/01 18:09:58 /someurl requested

Libraries like gorilla/mux или echo, gin can singnifically help with middleware implementation. Also, the most popular tasks for middleware are already covered in many libraries.

HTTP-client

In net/http package there is Client struct, that is responsible for HTTP-client requests:

client := http.Client{}

resp, err := client.Get("https://golangforall.com/en/")
if err != nil {
	log.Fatal(err)
}

buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
	log.Fatal(err)
}

fmt.Println(string(buf))

// some html is printed

Standard HTTP-client in GO looks simple, but it covers all possible tasks.

Some of the configuration is covered by Transport field in Client struct.

Related articles