Clean http handlers in Go
Introduction
For this blog post we are going to take a look at the http.HandlerFunc type and how we can improve it to make more elegant and clean handlers. Following the idioms of Go and staying compatible with the standard library.
Handlers
In Go A Handler is a type which responds to an HTTP request.
type Handler interface {
        ServeHTTP(http.ResponseWriter, *http.Request)
}
Any structure implementing the ServeHTTP method from the interface can be used to handle http requests. This is very powerful and flexible. It is easy to add http handling capabilities to any structure in your program.
Altough most of the time this is not the way a lot of people do it. It is still limited to the fact that you need to implement it on a structure which is not always what you would like. A single function would be easier to use than implementing an interface. Luckily Go has an solution for this.
Say Hi to the http.HandlerFunc
type HandlerFunc func(http.ResponseWriter, *http.Request)
The HandlerFunc is basically an adapter for the Handler interface. Because the HandlerFunc is a type it can implement methods on that type. Note that the type is actually a function so any function containing the same signature as the HandlerFunc can be easily casted to this type. When passing it as a parameter this happens implicitly and you wont even know the difference.
Ok cool, so here we are we’ve seen how the Handler and HandlerFunc works, but how can we actually extend them? And why do we even need to extend them, they are already powerfull aren’t they?
If you have ever written more complex http request handlers in Go you probably know that they can grow really big because of the verbose error handling and early returns.
func IndexHandler(w http.ResponseWriter, r *http.Request) {
  // Do something
  v, err := ...
  // check for err
  if err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte(err.Error()))
    return
  }
  // More code
  // ...
}
This is common code and because the functions returns void we have to terminate the function early if we wan’t to stop the function from writing more to the given io.Writer. Ofcourse an else would be possible here aswell but that would just decrease the readability, because statements will be more and more nested and harder to follow.
But most of the functions in Go returns errors when something failed instead of notifying through an given pointer in the parameters (after all we aren’t programming in C right)?
So how can we change this and still make use of the great integration with the standard library and the http.Handler interface. For this we are going to take the idea of http.HandlerFunc and create our own adapter for the Handler interface which can work with return types.
But first we need to find the perfect return types for our functions. Only an error would not be sufficient because we still have to set the status code on the ResponseWriter. We could make a generic struct which can contain most of the information we would like to send to the caller, this would look like this:
// Map of string to string where the key is the key for the header
// And the value is the value for the header
type Headers map[string]string
// Generic response object for our handlers
type Response struct {
	// StatusCode
	Status int
	// Content Type to writer
	ContentType string
	// Content to be written to the response writer
	Content io.Reader
	// Headers to be written to the response writer
	Headers Headers
}
The same as how the HandlerFunc works we create a type alias for our function definition. The type will return the new created response object by us.
type Action func(r *http.Request) *Response
We omitted the response writer as paramater because we don’t need it in our functions. We won’t be writing to the response writer from inside our function (this breaks the paradigm we want to accomplish). The response struct is the way for us to write content to the response writer. Now we need to make our Action type compatible with thehttp.Handler interface by implementing the ServeHTTP method on it.
func (a Action) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	if response := a(r); response != nil {
		if response.ContentType != "" {
			rw.Header().Set("Content-Type", response.ContentType)
		}
		for k, v := range response.Headers {
			rw.Header().Set(k, v)
		}
		rw.WriteHeader(response.Status)
		_, err := io.Copy(rw, response.Content)
		if err != nil {
			rw.WriteHeader(http.StatusInternalServerError)
		}
	} else {
		rw.WriteHeader(http.StatusOK)
	}
}
A few things going on here. First we declare the method on the function and we call the type Action which is in essence just a plain function. So every function with the same type signature as the Action can be converted to this Action type. As we know our Action type gave back a pointer to a Response struct. For this to work correctly we need to check if the pointer is not nil. Otherwise the program would panic when calling the Status and Message property on them. By using a pointer it gives us also the extra benefit of that we can return nil in our functions and nothing will be done (the default value of 200 OK will be send to the caller). We still have the same flexibility of the regular http.HandlerFunc and we are in control of when something will be written to the response writer.
Ok cool, with this in place we have some nice functions to work with. We can now return in our handlers and everything will be fine. We can now write some wrapper functions so we don’t have to manually create our Response struct.
Because our response struct works with an io.Reader interface we cannot just simply return the error in there so we need to first create a wrapper for this. We use a io.Reader here because this way we stay flexible, we can return any reader (even a stream reader) in our handlers and it will be streamed to the response writer.
func Error(status int, err error, headers Headers) *Response {
	return &Response{
		Status:  status,
		Content: bytes.NewBufferString(err.Error()),
		Headers: headers,
	}
}
This function pretty much explains itself. We pass in an error and we let the function convert it to an io.Reader with using an internal buffer. We can use it like this:
func Index(r *http.Request) *Response {
	return Error(404, errors.New("not found"), nil)
}
Sweet! that looks way more clear than before. Let’s take it a step further, nowadays a lot of people are making rest api’s which spit out JSON to the caller. We can easily create a function for this.
type errorResponse struct {
	Error string `json:"error"`
}
func ErrorJSON(status int, err error, headers Headers) *Response {
	errResp := errorResponse{
		Error: err.Error(),
	}
	b, err := json.Marshal(errResp)
	if err != nil {
		return Error(http.StatusInternalServerError, err, headers)
	}
	return &Response{
		Status:      status,
		ContentType: "application/json",
		Content:     bytes.NewBuffer(b),
		Headers:     headers,
	}
}
NOTE We can use the ErrorJSON functions again in our handlers. And it will do the conversion to JSON for us.
func Index(r *http.Request) *Response {
	return ErrorJSON(http.StatusNotFound, errors.New("not found"), nil)
}
and it will print:
{
  "error": "not found"
}
With those helper functions we can create responses for every content-type you would like; ErrorXML etc. We’ve seen error handling and how we can elegant create custom responses for our errors. How does this work for returning something else than en error?
We can create a generic functions (same as the error function) for regular data aswell.
func Data(status int, content []byte, headers Headers) *Response {
	return &Response{
		Status:  status,
		Content: bytes.NewBuffer(content),
		Headers: headers,
	}
}
Example usage:
func Index(r *http.Request) *Response {
	return Data(http.StatusOK, []byte("test"), nil)
}
Same as the errors we could take this a step further and implement some helper functions who will do marshalling of data to JSON.
func DataJSON(status int, v interface{}, headers Headers) *Response {
	b, err := json.Marshal(v)
	if err != nil {
		return ErrorJSON(http.StatusInternalServerError, err, headers)
	}
	return &Response{
		Status:      status,
		ContentType: "application/json",
		Content:     bytes.NewBuffer(b),
		Headers:     headers,
	}
}
Here we accept an interface in our method and let the json package take care of the conversion between the incoming v and the byte array. If we encounter some error during marshalling we just return our ErrorJSON function and the caller will be notified with the error (note this should probably be logged instead of returning the actual error to the caller). We do the same trick as in our ErrorJSON method and set the right content type. Usage is the same as all the other methods.
type temp struct {
	Message string `json:"msg"`
}
func Index(r *http.Request) *Response {
	return DataJSON(http.StatusOK, temp{"test"}, nil)
}
We can also create our helper function for the standard io.Reader this way we can return any reader we would like. This could be a external http or anything implementing theio.Reader interface.
func DataWithReader(status int, r io.Reader, headers Headers) *Response {
	return &Response{
		Status:  status,
		Content: r,
		Headers: headers,
	}
}
With this in place we have all the flexibility we would like and can return anything we can even think off. We eliminated the verbose writing to the response writer and made our handlers look way cleaner and easier to follow. Without losing perfomance or flexibility.
Compatibility with the standard library
Because our handlers are still of type http.Handler we can use them anywhere where the http.Handler interface is used.
Lets try it out! We are going to create middleware for loggin the details about a request
func logger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("[%s] User agent => %s Remote addr => %s", r.Method, r.UserAgent(), r.RemoteAddr)
		next.ServeHTTP(w, r)
	})
}
We can create a route in main
func main() {
	http.Handle("/test", logger(Action(Index)))
	http.ListenAndServe(":8080", nil)
}
And it all works. We can chain the middleware and use existing middlewares with our new handler types.
Conclusion
The http.Handler interfaces gives a lot of flexibility and by using type aliasing in Go we can easily convert our functions to actual methods which implement the Handler interface. It is even possible to extend our Response object with more options. This is all up to you and you can modify the wrappers to use the new options you define. (Extra headers for example).
the full code can be found here Github Gist. Let me know what you think about it and what could be improved.
Thanks for reading and happy coding! (y)
Clean http handlers in Go的更多相关文章
- 转: GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean
		
十年前,Martin Fowler撰写了 GUI Architectures 一文,至今被奉为经典.本文所谈的所谓架构二字,核心即是对于对于富客户端的 代码组织/职责划分 .纵览这十年内的架构模式变迁 ...
 - A little bit about Handlers in JAX-WS
		
by Rama Pulavarthi Handlers are message interceptors that can be easily plugged in to the JAX-WS run ...
 - 【C#】转一篇MSDN杂志文:ASP.NET Pipeline: Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code
		
序:这是一篇发表在2003年6月刊的MSDN Magazine的文章,现在已经不能在线阅读,只提供chm下载.讲的是异步请求处理那些事,正是我上一篇博文涉及的东西(BTW,事实上这篇杂志阐述了那么搞然 ...
 - Android 程序架构: MVC、MVP、MVVM、Unidirectional、Clean...
		
摘选自:GUI 应用程序架构的十年变迁:MVC.MVP.MVVM.Unidirectional.Cleanhttps://zhuanlan.zhihu.com/p/26799645 MV* in An ...
 - GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean
		
十年前,Martin Fowler撰写了 GUI Architectures 一文,至今被奉为经典.本文所谈的所谓架构二字,核心即是对于对于富客户端的 代码组织/职责划分 .纵览这十年内的架构模式变迁 ...
 - Error:Execution failed for task ':app:clean'.
		
运行时出现 Error:Execution failed for task ':app:clean'. 错误,Builld->Clean Project即可.
 - 学习Maven之Maven Clean Plugin
		
1.maven-clean-plugin是个什么鬼? maven-clean-plugin这个插件用maven的人都不陌生.我们在执行命令mvn clean时调用的就是这个插件. 这个插件的主要作用就 ...
 - AndroidStudio中make Project、clean Project、Rebuild Project的区别
		
1.Make Project:编译Project下所有Module,一般是自上次编译后Project下有更新的文件,不生成apk. 2.Make Selected Modules:编译指定的Modul ...
 - Clean Old Kernels on CentOS
		
1. Check Installed Kernels $ rpm -q kernel 2. Clean Old Kernels ## need Install yum-utils ## ## Pack ...
 
随机推荐
- Java不走弯路教程(5.Client-Server模式(2)-Client)
			
5.Client-Server模式(2)-Client 在上一章,我们完成一个简单的数据库服务器,并在客户端用telnet方式成功进行通信. 本章将用Java实现客户端程序,来代替telnet. 先看 ...
 - mybatis中#{}与${}的区别
			
今天学习了下mybatis的查询,了解到了#{}与${}的区别, 配置文件如下: <?xml version="1.0" encoding="UTF-8" ...
 - JVM学习--(六)类加载器原理
			
我们知道我们编写的java代码,会经过编译器编译成字节码文件(class文件),再把字节码文件装载到JVM中,映射到各个内存区域中,我们的程序就可以在内存中运行了.那么字节码文件是怎样装载到JVM中的 ...
 - Table对象代表一个HTML表格,在文档中<table>标签每出现一次,一个table对象就会被创建。
			
1.对象集合 cells[] 返回包含表格中所有单元格的一个数组 rows[] 返回包含表格中所有行的一个数组 tBodies[] 返回包含表格中所有tbody的一个数组(主包含ty和td) 2.对象 ...
 - SDWebImage底层实现原理
			
SDWebImage底层实现有沙盒缓存机制,主要由三块组成 1.内存图片缓存2.内存操作缓存3.磁盘沙盒缓存内部实现过程:第一步,下载SDWebImage,导入工程. 第二步,在需要的地方导入头文件 ...
 - JQuery(一)---- JQ的选择器,属性,节点,样式,函数等操作详解
			
JQuery的基本概念 JQuery是一个javascript库,JQuery凭借着简洁的语法和跨平台的兼容性,极大的简化了js操作DOM.处理事件.执行动画等操作.JQuery强调的理念是:'wri ...
 - 经典栈溢出之MS060-040漏洞分析
			
找了好久才找到Win 2000 NetApi32.dll样本,下面我对这个经典栈溢出进行一下分析,使用IDA打开NetApi32.dll,问题函数:NetpwPathCanonucalize.实验环境 ...
 - [总结] 二维ST表及其优化
			
二维 \(\mathcal{ST}\) 表,可以解决二维 \(\mathcal{RMQ}\) 问题.这里不能带修改,如果要修改,就需要二维线段树解决了. 上一道例题吧 ZOJ2859 类比一维 \(\ ...
 - Day16   Django
			
学Django之前,先看下http基础,老师的网页地址: web框架 - Yuan先生 - 博客园 http://www.cnblogs.com/yuanchenqi/articles/7690561 ...
 - 数据库中row_number()、rank()、dense_rank() 的区别
			
row_number的用途非常广泛,排序最好用它,它会为查询出来的每一行记录生成一个序号,依次排序且不会重复,注意使用row_number函数时必须要用over子句选择对某一列进行排序才能生成序号. ...