本文做的事情:在开发了一个简单的web服务程序cloudgo之后,进行功能完善和源码分析。
项目地址为:github.com/Winszheng/c…
一、统一错误处理
错误处理是golang开发中必不可少的环节,最基本的错误处理是panic和在控制台打印错误信息,我们不希望有太多panic,因为这样会妨碍程序正常工作;也不希望因为处理错误信息的代码太多太分散而降低代码可读性。因此,可以考虑尽量把panic处理成error,把逻辑处理和错误处理解耦,用一个errWrapper函数包装错误,逻辑处理函数只需要处理逻辑并返回错误,而具体错误的处理将由errWrapper完成。
在展示文件静态处理的函数中,处理函数为:
func HandleFileList(writer http.ResponseWriter,
request *http.Request) error {
// 判断前缀是否合法
fmt.Println("path:",request.URL.Path)
if strings.Index(request.URL.Path, prefix) != 0 {
return userError("path must start with "+prefix)
}
// 取路径
path := request.URL.Path[len(prefix):]
file, err := os.Open("assets/"+path)
if err!= nil {
return err
}
defer file.Close()
content, err := ioutil.ReadAll(file)
writer.Write(content)
return nil // 没有出错,返回nil
}
而返回的错误将被这样处理:
func errWrapper(
handler appHandler) func(writer http.ResponseWriter, request *http.Request) {
return func(writer http.ResponseWriter,
request *http.Request){
// http库自带的panic处理内容太多,没有必要
// 自行修改panic的处理
defer func() {
if r := recover(); r != nil {
// 日志打印到控制台
log.Printf("panic:%v\n", r)
http.Error(writer,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
}()
// 调用处理函数
err := handler(writer, request)
if err != nil {
// 日志信息输出到控制台
log.Printf("error handling request:%s\n",err.Error())
// 判断是系统错误,还是用户可见
if userErr, ok := err.(userError); ok {
// 用户可见
http.Error(writer, userErr.Message(), http.StatusBadRequest)
return
}
code := http.StatusOK // 默认200, 正常
switch {
case os.IsNotExist(err):
code = http.StatusNotFound // 404 not found
case os.IsPermission(err):
code = http.StatusForbidden // 403 没权限
default:
code = http.StatusInternalServerError // else
}
http.Error(writer, http.StatusText(code), code)
}
}
}
为了区分哪些错误是系统可见的,哪些错误是用户可见的,例如,假定服务器中只有assets文件夹中的静态资源才可以被访问,那么当用户试图访问其他地方的静态资源时,给予"path must start with /assets/"的提示显然是合理的,这就属于用户可见的错误,为了实现错误信息的区分,需要一个userError接口:
type userError interface {
error // 系统看的
Message() string // for user
}
需要一个userError接口将标准库已有的error接口和一个函数签名Message() string组合起来,那么,使用者只要实现userError接口即可,本项目这样实现:
type userError string
func (e userError) Error() string {
return string(e)
}
func (e userError) Message() string{
return string(e)
}
二、日志文件输出
2.1 日志文件的要求
在开发中,我们常常需要一个好的日志记录器,我们通常希望logger能够实现这样的功能:
- 文件化日志输出,而不是把所有内容都输出到控制台
- 日志切割,能根据文件大小、时间或间隔分割日志文件
- 日志分级,能清晰判断是info、debug还是error等
- 信息详尽,包含调用文件/函数名/行号/时间等 golang提供了log库,但内容不多,实现的功能比较简单。现在比较流行的第三方日志库也不少,本文关心的是zap库。那么,可以这样做:用log库和zap库分别为本次开发的程序实现日志文件输出,并做比较。
2.2 用log库实现
在原项目增加logfile文件夹,存放日志文件;增加logger文件夹,存放实现日志输出的代码,如此以来,项目结构是这样的:
实现思路很简单,先在logger.go设置日志文件路径:
func SetupLogger() {
// 设置log文件output路径
logFileLocation, _ := os.OpenFile("logfile/zap.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
log.SetOutput(logFileLocation)
}
然后在启动函数调用:
之后正常启动,原本log.Printf的内容都会打印在控制台,现在则都会打印到zap.log,就像这样:
这样做的优点
简单!
存放在哪里完全可以随意指定,只要是实现了io.Writer的类型就可以了!
缺点
实现起来简单,所以也记录不了复杂的内容,比如无法分级日志,也不能记录太多详细的信息。
2.3 用zap库实现
zap提供了快速的、结构化的、层次化的日志记录,特点之一:比标准库更快。
zap提供了两种类型的日志记录器:SugaredLogger和Logger,选择的依据:
In contexts where performance is nice, but not critical, use the SugaredLogger
在性能好但性能并非关键的情形中,使用SugaredLogger.
When performance and type safety are critical, use the Logger.
在性能和安全都很关键的情形下,使用Logger.
2.4 看看源码
那么,要使用zap库实现日志文件输出,先看zap库的关键函数New的源码:
// New constructs a new Logger from the provided zapcore.Core and Options. If
// the passed zapcore.Core is nil, it falls back to using a no-op
// implementation.
//
// This is the most flexible way to construct a Logger, but also the most
// verbose. For typical use cases, the highly-opinionated presets
// (NewProduction, NewDevelopment, and NewExample) or the Config struct are
// more convenient.
//
// For sample code, see the package-level AdvancedConfiguration example.
func New(core zapcore.Core, options ...Option) *Logger {
if core == nil {
return NewNop()
}
log := &Logger{
core: core,
errorOutput: zapcore.Lock(os.Stderr),
addStack: zapcore.FatalLevel + 1,
}
return log.WithOptions(options...)
}
// NewCore creates a Core that writes logs to a WriteSyncer.
func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core {
return &ioCore{
LevelEnabler: enab,
enc: enc,
out: ws,
}
}
New函数根据传入的core和options构造新Logge并返回指针,core指构造编码器时必须要有的配置信息,options指额外设定的信息,比如是否记录调用函数等,使用NewCore函数构造core,传入的三个参数作用分别为:指定编码器、指定日志级别、指定日志输出。
2.5 具体实现
而具体到这次开发,整个logger.go的逻辑是:
var logger *zap.Logger
var SugarLogger *zap.SugaredLogger // SugarLogger需要全局使用记录日志
func InitLogger() {
writeSyncer := getLogWriter()
encoder := getEncoder()
// zapcore.DebugLevel: 日志级别
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
logger := zap.New(core, zap.AddCaller()) // zap.AddCaller(): 记录调用函数
SugarLogger = logger.Sugar() // 通过主logger的方法获取SugarLogger
}
// getEncoder获取编码器(如何写入日志)
// 时间使用人类可读的方式
// 使用大写字母标识日志级别
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
// 指定日志输出
func getLogWriter() zapcore.WriteSyncer {
file, _ := os.Create("logfile/zap.log")
return zapcore.AddSync(file)
}
在整个程序中,只需要在入口函数添加:
logger.InitLogger()
defer logger.SugarLogger.Sync()
之后在需要输出日志的地方以类似logger.SugarLogger.Panicf("panic:%v\n", r)、logger.SugarLogger.Errorf("error handling request:%s\n", err.Error())的方式调用即可,本质上和log.Printf的使用是一样的。
2.6 结果
日志明确地展示了时间、类型、文件、行号和自定义输出内容,和原生的log库相比显然可读性高很多,而且这样的输出已经基本满足普通程序的需求,logger.go文件以后可以直接复用。
三、gorilla/mux库源码分析
3.1 http vs. gorilla/mux
golang自带的http库能够实现一些路由功能,事实上,这次程序只使用http.HandFunc也能完成,但http库存在这样地问题:
- 不支持路由参数设定
- 不能友好地支持REST模式
- 无法限制访问方法(POST\GET...)
- 不支持正则 而这些问题,gorilla/mux库都能解决,另外还支持中间件等功能,所以比原生http的使用更广泛,把代码从使用原生http库改成使用mux库:
router := mux.NewRouter()
// 因为mux库严格按照正则匹配,当需要处理前缀变长的路由时需要额外设置
router.PathPrefix("/assets").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/"))))
// 限制路由器只处理指定的请
router.HandleFunc("/api/test", service.ApiTestHandler(formatter))
router.HandleFunc("/api/table", handler.HandleTable(formatter))
router.HandleFunc("/unknow", handler.UnknownHandler(formatter))
err := http.ListenAndServe(":8080", router)
if err != nil {
logger.SugarLogger.Error(err)
} else {
logger.SugarLogger.Debug("Normal start-up")
}
http库默认匹配变长前缀,而mux库由于支持正则表达,所以需要额外设置。这里有一个坑,因为我分别用http库和mux库做了一次,于是出现这样一个问题:chrome缓存导致页面不能如预期显示,本来不应当重定向的页面发生了重定向,打开f12,发现状态码是301,显示根据磁盘缓存跳转,于是清除了近一天的缓存,解决了问题。
使用mux库时需要注意的是,mux支持正则匹配,所以并不是自动支持变长的路由匹配,mux.NewRouter().HandleFunc(path string, f func(http.ResponseWriter, *http.Request))仅匹配指定的url,变长前缀的匹配可以参考老师给的例子。
3.2 至于源码
mux库代码框架
- mux.go: 核心函数定义
- route.go: 路由相关函数和类
- middleware.go: 中间件定义
- regexp.go: 正则处理 mux库有两个重要的类,一个是Route, 是表征路由器的类型:
type Router struct {
// Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler
// Configurable Handler to be used when the request method does not match the route.
MethodNotAllowedHandler http.Handler
// Routes to be matched, in order.
routes []*Route
// Routes by name for URL building.
namedRoutes map[string]*Route
// If true, do not clear the request context after handling the request.
//
// Deprecated: No effect, since the context is stored on the request itself.
KeepContext bool
// Slice of middlewares to be called after a match is found
middlewares []middleware
// configuration shared with `Route`
routeConf
}
另一个是Route,表征某条具体路由:
// Route stores information to match a request and build URLs.
type Route struct {
// Request handler for the route.
handler http.Handler
// If true, this route never matches: it is only used to build URLs.
buildOnly bool
// The name used to build URLs.
name string
// Error resulted from building a route.
err error
// "global" reference to all named routes
namedRoutes map[string]*Route
// config possibly passed in from `Router`
routeConf
}
而使用中重要的函数也有两个, 一个是func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request),代码太常就不贴了,函数首先判断是否需要清晰路径(比如去除不需要的斜杠并重定位),然后获取路由匹配器,并用相应的handler调用http.Handler.ServeHTTP。
另一ige重要的函数是func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,*http.Request)) *Route,和http.HandleFunc不同,这个函数被设计成只能支持指定路由(自然是因为mux能够支持正则表达式)。
四、函数式编程
函数式编程是一种“编程范式”,主要思想是把过程尽量写成一系列嵌套的函数调用,go语言对函数式编程的支持主要体现在闭包上。
理解闭包
可以这样认为:
golang闭包 = 内部函数 + 绑定的变量
这样看起来也许有些玄学,从内存回收的角度考虑会更容易理解,正常函数调用完后内部的变量就会销毁(变量在栈上),但闭包用到的外部变量会被一直保存(用到的外部变量放在了堆上)。
当然,因为gc的存在,程序员并不需要关心变量具体在堆上还是栈上。
一个例子
// 该函数类型实现了一个方法
func (c *CalculateType) Serve() {
fmt.Println("我是一个函数类型")
}
// 加法函数
func add(a, b int) {
fmt.Println(a + b)
}
// 乘法函数
func mul(a, b int) {
fmt.Println(a * b)
}
func main() {
a := CalculateType(add) // 将add函数强制转换成CalculateType类型
b := CalculateType(mul) // 将mul函数强制转换成CalculateType类型
a(2, 3)
b(2, 3)
a.Serve()
b.Serve()
}
// 5
// 6
// 我是一个函数类型
// 我是一个函数类型
但在实际中,我们很少用这种方法来使用闭包/函数式编程,上面这个例子虽然好理解,但思想上其实近似c++。
另一个例子
来自http库:
// (1)按给定pattern注册handler函数,然后调用函数(2)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
// (2)按给定pattern注册handler函数
// 然后调用func (mux *ServeMux) Handle(pattern string, handler Handler)
// 这里需要注意的是,用到了(3)定义的类型做强制类型转换
// 调用的太长不复制,反正在这里也不重要
// 强制类型转换之后,我们自己的handler就也实现了ServeHTTP(),即实现了Handler接口
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
// (3)
type HandlerFunc func(ResponseWriter, *Request)
// (4) f调用自己,即调用了自己的处理函数
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
简单粗暴点理解,就是参数是函数,返回也是函数,这在标准库里用的不少,多看点就好。
最后,写博客好累,就算理解,但是要图文代码并茂且好理解的写出来,好难(裂开)。