Skip to content

Latest commit

 

History

History
204 lines (173 loc) · 7.67 KB

File metadata and controls

204 lines (173 loc) · 7.67 KB

3.4 Get into http package

In previous sections, we learned about the work flow of the web and talked a little bit about Go's http package. In this section, we are going to learn about two core functions in the http package: Conn and ServeMux.

goroutine in Conn

Unlike normal HTTP servers, Go uses goroutines for every job initiated by Conn in order to achieve high concurrency and performance, so every job is independent.

Go uses the following code to wait for new connections from clients.

c, err := srv.newConn(rw)
if err != nil {
	continue
}
go c.serve()

As you can see, it creates a new goroutine for every connection, and passes the handler that is able to read data from the request to the goroutine.

Customized ServeMux

We used Go's default router in previous sections when discussing conn.server, with the router passing request data to a back-end handler.

The struct of the default router:

type ServeMux struct {
	mu sync.RWMutex        // because of concurrency, we have to use a mutex here
	m  map[string]muxEntry // router rules, every string mapping to a handler
}

The struct of muxEntry:

type muxEntry struct {
	explicit bool // exact match or not
	h        Handler
}

The interface of Handler:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)  // routing implementer
}

Handler is an interface, but if the function sayhelloName didn't implement this interface, then how did we add it as handler? The answer lies in another type called HandlerFunc in the http package. We called HandlerFunc to define our sayhelloName method, so sayhelloName implemented Handler at the same time. It's like we're calling HandlerFunc(f), and the function f is force converted to type HandlerFunc.

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

How does the router call handlers after we set the router rules?

The router calls mux.handler.ServeHTTP(w, r) when it receives requests. In other words, it calls the ServeHTTP interface of the handlers which have implemented it.

Now, let's see how mux.handler works.

func (mux *ServeMux) handler(r *Request) Handler {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// Host-specific pattern takes precedence over generic ones
	h := mux.match(r.Host + r.URL.Path)
	if h == nil {
		h = mux.match(r.URL.Path)
	}
	if h == nil {
		h = NotFoundHandler()
	}
	return h
}

The router uses the request's URL as a key to find the corresponding handler saved in the map, then calls handler.ServeHTTP to execute functions to handle the data.

You should understand the default router's work flow by now, and Go actually supports customized routers. The second argument of ListenAndServe is for configuring customized routers. It's an interface of Handler. Therefore, any router that implements the Handler interface can be used.

The following example shows how to implement a simple router.

package main

import (
	"fmt"
	"net/http"
)

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		sayhelloName(w, r)
		return
	}
	http.NotFound(w, r)
	return
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello myroute!")
}

func main() {
	mux := &MyMux{}
	http.ListenAndServe(":9090", mux)
}

Routing

If you do not want to use a Router, you can still achieve what we wrote in the above section by replacing the second argument to ListenAndServe to nil and registering the URLs using a HandleFunc function which goes through all the registered URLs to find the best match, so care must be taken about the order of the registering.

sample code:

http.HandleFunc("/", views.ShowAllTasksFunc)
http.HandleFunc("/complete/", views.CompleteTaskFunc)
http.HandleFunc("/delete/", views.DeleteTaskFunc)
    
//ShowAllTasksFunc is used to handle the "/" URL which is the default ons
//TODO add http404 error
func ShowAllTasksFunc(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		context := db.GetTasks("pending") //true when you want non deleted tasks
		//db is a package which interacts with the database
		if message != "" {
			context.Message = message
		}
		homeTemplate.Execute(w, context)
		message = ""
	} else {
		message = "Method not allowed"
		http.Redirect(w, r, "/", http.StatusFound)
	}
}

This is fine for simple applications which doesn't requires parameterized routing, what when you need that? You can either use the existing toolkits or frameworks, but since this book is about writing webapps in golang, we are going to teach how to handle this scenario as well.

When the match is made on the HandleFunc function, the URL is matched, so suppose we are writing a todo list manager and we want to delete a task so the URL we decide for that application is /delete/1, so we register the delete URL like this http.HandleFunc("/delete/", views.DeleteTaskFunc) /delete/1 this URL matches closest with the "/delete/" URL than any other URL so in the r.URL.path we get the entire URL of the request.

http.HandleFunc("/delete/", views.DeleteTaskFunc)
//DeleteTaskFunc is used to delete a task, trash = move to recycle bin, delete = permanent delete
func DeleteTaskFunc(w http.ResponseWriter, r *http.Request) {
	if r.Method == "DELETE" {
		id := r.URL.Path[len("/delete/"):]
		if id == "all" {
			db.DeleteAll()
			http.Redirect(w, r, "/", http.StatusFound)
		} else {
			id, err := strconv.Atoi(id)
			if err != nil {
				fmt.Println(err)
			} else {
				err = db.DeleteTask(id)
				if err != nil {
					message = "Error deleting task"
				} else {
					message = "Task deleted"
				}
				http.Redirect(w, r, "/", http.StatusFound)
			}
		}
	} else {
		message = "Method not allowed"
		http.Redirect(w, r, "/", http.StatusFound)
	}
}

link: https://github.com/thewhitetulip/Tasks/blob/master/views/views.go#L170-#L195

In this above method what we basically do is in the function which handles the /delete/ URL we take its compelete URL, which is /delete/1, then we take a slice of the string and extract everything which starts after the delete word which is the actual parameter, in this case it is 1. Then we use the strconv package to convert it to an integer and delete the task with that taskID.

In more complex scenarios too we can use this method, the advantage is that we don't have to use any third party toolkit, but then again third party toolkits are useful in their own right, you need to make a decision which method you'd prefer. No answer is the right answer.

Go code execution flow

Let's take a look at the whole execution flow.

  • Call http.HandleFunc
    1. Call HandleFunc of DefaultServeMux
    2. Call Handle of DefaultServeMux
    3. Add router rules to map[string]muxEntry of DefaultServeMux
  • Call http.ListenAndServe(":9090", nil)
    1. Instantiate Server
    2. Call ListenAndServe method of Server
    3. Call net.Listen("tcp", addr) to listen to port
    4. Start a loop and accept requests in the loop body
    5. Instantiate a Conn and start a goroutine for every request: go c.serve()
    6. Read request data: w, err := c.readRequest()
    7. Check whether handler is empty or not, if it's empty then use DefaultServeMux
    8. Call ServeHTTP of handler
    9. Execute code in DefaultServeMux in this case
    10. Choose handler by URL and execute code in that handler function: mux.handler.ServeHTTP(w, r)
    11. How to choose handler: A. Check router rules for this URL B. Call ServeHTTP in that handler if there is one C. Call ServeHTTP of NotFoundHandler otherwise

Links