IT虾米网

Golang构建简单web框架详解

itxm 2018年07月09日 大数据 263 0
使用Golang构建web服务还是比较简单的,使用net/http和gorilla/mux就能快速的构建一个简易的web server
package main 
 
import { 
    "net/http" 
    "github.com/gorilla/mux" 
} 
 
func main() { 
    router = mux.NewRouter().StrictSlash(true) 
    router.Handle("/", http.FileServer(http.Dir("/static"))) 
    http.ListenAndServe(":8080", nil) 
} 

这样一个简易的静态服务器就构建成功了。

当然我们不可能就这么满足了,我们当然希望这个服务器是可以处理一些业务逻辑的。比如登录:

router.HandleFunc("/login", handlers.LoginHandler) 

handler怎么写呢:

func LoginHandler(w http.ResponseWriter, r *http.Request) { 
    controllers.LoginIndexAction(w,r); 
} 

controller(使用mymysql连接数据库):

func LoginAction(w http.ResponseWriter, r *http.Request) { 
    w.Header().Set("content-type", "application/json") 
    err := r.ParseForm() 
 
    if err != nil { 
        Response(w, "Param error.", "PARAM_ERROR",403) 
        return 
 
    } 
    admin_name      := r.FormValue("admin_name") 
    admin_password  := r.FormValue("admin_password") 
    if admin_name == "" || admin_password == ""{ 
        Response(w, "Param error.", "PARAM_ERROR",403) 
        return 
    } 
 
    db := mysql.New("tcp", "", "127.0.0.1:3306", "user", "pass", "database") 
    if err := db.Connect(); err != nil { 
        log.Println(err) 
        Response(w, "Param error.", "PARAM_ERROR",403) 
        return 
    } 
    defer db.Close() 
 
    rows, res, err := db.Query("select * from webdemo_admin where admin_name = '%s'", admin_name) 
 
    if err != nil { 
        log.Println(err) 
        Response(w, "Database error.", "DATABASE_ERROR",503) 
        return 
    } 
 
    name := res.Map("admin_password") 
    admin_password_db := rows[0].Str(name) 
 
    if admin_password_db != admin_password { 
        Response(w, "Password error.", "PASSWORD_ERROR",403) 
        return 
    } 
 
    cookie := http.Cookie{Name: "admin_name", Value: rows[0].Str(res.Map("admin_name")), Path: "/"} 
 
    http.SetCookie(w, &cookie) 
    Response(w, "Login success.", "SUCCESS",200) 
    return 
 
} 
 
type response struct{ 
    Status int `json:"status"` 
    Description string `json:"description"` 
    Code string `json:"code"` 
} 
 
func Response(w http.ResponseWriter, description string,code string, status int) { 
    out := &response{status, description, code} 
    b, err := json.Marshal(out) 
    if err != nil { 
        return 
    } 
    w.WriteHeader(status) 
    w.Write(b) 
} 

将用户名放到cookie里就当登录成功了。

如果有多个路由需要处理呢,情形就会变成这样:

router.HandleFunc("/url1", handlers.Handler1) 
router.HandleFunc("/url2", handlers.Handler1) 
router.HandleFunc("/url3", handlers.Handler1) 
router.HandleFunc("/url4", handlers.Handler1) 
router.HandleFunc("/url5", handlers.Handler1) 
router.HandleFunc("/url6", handlers.Handler1) 
router.HandleFunc("/url7", handlers.Handler1) 
... 

好像也无伤大雅,但是如果有更一步的需求,每个URL需要做权限验证,记录日志,这种方式显然就不太合理了,我们需要对router做统一的管理,这里我们跳过了handler层,直接由controller来处理,我觉得更简洁一点。

//先定义Route的结构体 
type Route struct { 
	Name        string 
	Method      string 
	Pattern     string 
	Auth		bool 
	HandlerFunc http.HandlerFunc 
} 
 
type Routes []Route 
 
var routes = Routes{ 
	Route{ 
		"url1", 
		"GET", 
		"/url1", 
		true, 
		controllers.Url1, 
	}, 
	Route{ 
		"url2", 
		"POST", 
		"/url2", 
		false, 
		controllers.Url2, 
	}, 
} 
 
var router *mux.Router 
 
func NewRouter() *mux.Router { 
        if router == nil { 
                router = mux.NewRouter().StrictSlash(true) 
        } 
	for _, route := range routes { 
		router. 
			Methods(route.Method). 
			Path(route.Pattern). 
			Name(route.Name). 
			Handler(route.HandlerFunc) 
	} 
 
	return router 
} 

这时候如果添加权限验证,只有通过登录验证的用户才有权限调用,这就需要中间件(我个人比较喜欢称它装饰器)出场了:

func Auth(inner http.Handler) http.Handler { 
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
	    cookie, err := r.Cookie("admin_name") 
	    if err != nil || cookie.Value == ""{ 
	        Response(w, "token not found.", "AUTH_FAILED",403) 
	        return; 
	    } 
 
	    rows, res, err := db.Query("select * from user where user_name= '%d'", cookie.Value) 
 
	    if err != nil { 
	        Response(w, "can not connect database.", "DB_ERROR",500) 
	        return 
	    } 
 
	    if len(rows) == 0 { 
	    	Response(w, "user not found.", "NOT_FOUND",404) 
	    	return 
	    } 
 
	    row := rows[0] 
 
	    user := controllers.User{ 
	    		User_id:row.Int(res.Map("user_id")),  
	    		User_name:row.Str(res.Map("user_name")), 
	    		User_type:row.Str(res.Map("user_type")), 
	    		Add_time:row.Str(res.Map("add_time"))} 
	    session.CurrentUser = user 
	    log.Printf("user_id:%v",controllers.CurrentUser.User_id) 
	    inner.ServeHTTP(w, r) 
	}) 
} 
func NewRouter() *mux.Router { 
	if router == nil { 
                router = mux.NewRouter().StrictSlash(true) 
        } 
	for _, route := range routes { 
		if(route.Auth){ 
			handler = decorates.Auth(route.HandlerFunc) 
 
		} 
		router. 
			Methods(route.Method). 
			Path(route.Pattern). 
			Name(route.Name). 
			Handler(handler) 
	} 
 
	return router 
 
} 

显然这样管理session是比较粗糙的,怎么办,有现成的解决方案,jwt(JSON Web Tokens),我们可以使用jwt-go来生成token,如果一个请求cookie或者header里面含有token,并且可以验证通过,我就认为这个用户是合法用户:

//生成token 
func Generate(key string) (string, error) { 
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 
		"key": key, 
		"exp": (time.Now().Add(time.Minute * 60 * 24 * 2)).Unix(), 
	}) 
 
	tokenString, err := token.SignedString(settings.HmacSampleSecret) 
	return tokenString, err 
} 
//验证token 
func Valid(tokenString string) (string, error) { 
	token1, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 
		} 
		return settings.HmacSampleSecret, nil 
	}) 
 
	if claims, ok := token1.Claims.(jwt.MapClaims); ok && token1.Valid { 
		return fmt.Sprintf("%v", claims["key"]), nil 
	} else { 
		return "", err 
	} 
 
} 

Auth中间件就可以变成下面的样子:

func Auth(inner http.Handler) http.Handler { 
 
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
	    cookie, err := r.Cookie("token") 
	    if err != nil || cookie.Value == ""{ 
	        Response(w, "token not found.", "AUTH_FAILED",403) 
	        return; 
	    } 
 
	    user_id, err := token.Valid(cookie.Value) 
 
	    if err != nil { 
	        Response(w, "bad token.", "AUTH_FAILED",403) 
	        return; 
	    } 
 
	    rows, res, err := db.Query("select * from user where user_id= '%d'", user_id) 
 
	    if err != nil { 
	        Response(w, "can not connect database.", "DB_ERROR",500) 
	        return 
	    } 
 
	    if len(rows) == 0 { 
	    	Response(w, "user not found.", "NOT_FOUND",404) 
	    	return 
	    } 
 
	    row := rows[0] 
 
	    user := controllers.User{ 
	    		User_id:row.Int(res.Map("user_id")),  
	    		User_name:row.Str(res.Map("user_name")), 
	    		User_type:row.Str(res.Map("user_type")), 
	    		Add_time:row.Str(res.Map("add_time"))} 
 
	    session.CurrentUser = user 
 
	    log.Printf("user_id:%v",controllers.CurrentUser.User_id) 
	    inner.ServeHTTP(w, r) 
	}) 
} 

我们还可以对每个URL实现log记录:

func Logger(inner http.Handler, name string) http.Handler { 
 
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
		start := time.Now() 
		inner.ServeHTTP(w, r) 
 
		log.Printf( 
			"%s\t%s\t%s\t%s", 
			r.Method, 
			r.RequestURI, 
			name, 
			time.Since(start), 
		) 
	}) 
} 
 
func NewRouter() *mux.Router { 
	if router == nil { 
                router = mux.NewRouter().StrictSlash(true) 
        } 
	for _, route := range routes { 
		var handler http.Handler = decorates.Logger(route.HandlerFunc, route.Name) 
		if(route.Auth){ 
			handler = decorates.Auth(handler) 
		} 
		router. 
			Methods(route.Method). 
			Path(route.Pattern). 
			Name(route.Name). 
			Handler(handler) 
	} 
	return router 
} 

有跨域的需求?好办:

func CorsHeader(inner http.Handler) http.Handler { 
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
 
    	    w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) 
    	    w.Header().Set("Access-Control-Allow-Credentials", "true") 
	    w.Header().Add("Access-Control-Allow-Method","POST, OPTIONS, GET, HEAD, PUT, PATCH, DELETE") 
 
	    w.Header().Add("Access-Control-Allow-Headers","Origin, X-Requested-With, X-HTTP-Method-Override,accept-charset,accept-encoding , Content-Type, Accept, Cookie") 
 
    	    w.Header().Set("Content-Type","application/json") 
		inner.ServeHTTP(w, r) 
	}) 
 
} 
 
func NewRouter() *mux.Router { 
	if router == nil { 
        router = mux.NewRouter().StrictSlash(true) 
    } 
	for _, route := range routes { 
		var handler http.Handler = decorates.Logger(route.HandlerFunc, route.Name) 
		if(route.Auth){ 
			handler = decorates.Auth(handler) 
		} 
		handler = decorates.CorsHeader(handler) 
		router. 
			Methods(route.Method). 
			Path(route.Pattern). 
			Name(route.Name). 
			Handler(handler) 
		router. 
			Methods("OPTIONS"). 
			Path(route.Pattern). 
			Name("cors"). 
			Handler(decorates.CorsHeader(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
				return 
			}))) 
	} 
 
	return router 
} 

session管理好像还有一些问题,每个request请求都会改变全局的CurrenUser,如果有并发的情况下,这就容易产生混乱了,可以需要用户信息的时候通过token去数据库来取,效率会有影响,但并发的问题可以解决了:

func CurrentUser(r *http.Request) *models.User { 
	cookie, err := r.Cookie("token") 
	if err != nil || cookie.Value == "" { 
		return &models.User{} 
	} 
	key, err := token.Valid(cookie.Value) 
	if err != nil { 
		return &models.User{} 
	} 
	if !strings.Contains(key, "|") { 
		return &models.User{} 
	} 
	keys := strings.Split(key, "|") 
	rows, res, err := db.QueryNonLogging("select * from user where user_id = '%v' and user_pass = '%v'", keys[0], keys[1]) 
 
	if err != nil { 
		return &models.User{} 
	} 
 
	if len(rows) == 0 { 
		return &models.User{} 
	} 
	row := rows[0] 
	user := models.User{ 
		User_id:   row.Int(res.Map("user_id")), 
		User_name: row.Str(res.Map("user_name")), 
		User_type: row.Str(res.Map("user_type")), 
		Add_time:  row.Str(res.Map("add_time"))} 
 
	return &user 
 
} 

日志的问题好像还没有解决,毕竟日志需要写到文件里面并且需要一些详细的信息,比如行号,文件,才能利于排查问题,或者做统计:

func Printf(format string, params ...interface{}) { 
	_, f, line, _ := runtime.Caller(1) 
	log.Printf(format, params...) 
	file, err := os.OpenFile(settings.LogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.ModePerm) 
	if err != nil { 
		log.Printf("%v", err) 
		return 
	} 
	defer file.Close() 
	_, err = file.Seek(0, os.SEEK_END) 
	if err != nil { 
		return 
	} 
	args := strings.Split(f, "/") 
	f = args[len(args)-1] 
	msg := fmt.Sprintf("%v:%v(%v)", line, format, f) 
	logger := log.New(file, "", log.LstdFlags) 
	logger.Printf(msg, params...) 
} 
 
func Println(v ...interface{}) { 
	_, f, line, _ := runtime.Caller(1) 
	log.Println(v...) 
	file, err := os.OpenFile(settings.LogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, os.ModePerm) 
	if err != nil { 
		log.Printf("%v", err) 
		return 
	} 
	defer file.Close() 
	_, err = file.Seek(0, os.SEEK_END) 
	if err != nil { 
		return 
	} 
	args := strings.Split(f, "/") 
	f = args[len(args)-1] 
	msg := fmt.Sprintf("%v:%v(%v)", line, fmt.Sprintln(v...), f) 
	logger := log.New(file, "", log.LstdFlags) 
	logger.Println(msg) 
 
} 

日志写到文件的问题解决了,又面临新的问题,日志文件太大,怎么办,需要归档(每隔12小时就查看一下日志文件多大了,如果太大了就压缩一下归档):

var ticker = time.NewTicker(time.Minute * 60 * 12) 
 
func init() { 
	go func() { 
		for _ = range ticker.C { 
			archive() 
		} 
	}() 
 
} 
 
func archive() error { 
	info, _ := os.Stat(settings.LogFile) 
	if info.Size() > 1024*1024*50 { 
		target := fmt.Sprintf("%v.%v.tar.gz", 
			shortFileName(settings.LogFile), 
			time.Now().Format("2006-01-02-15-04"), 
		) 
		tmp := fmt.Sprintf("%v.%v.tmp", 
			shortFileName(settings.LogFile), 
			time.Now().Format("2006-01-02-15-04"), 
		) 
		in := bytes.NewBuffer(nil) 
		cmd := exec.Command("sh") 
		cmd.Stdin = in 
		go func() { 
			in.WriteString(fmt.Sprintf("cd %v\n", shortFileDir(settings.LogFile))) 
			in.WriteString(fmt.Sprintf("cp %v %v\n", shortFileName(settings.LogFile), tmp)) 
			in.WriteString(fmt.Sprintf("echo '' > %v\n", shortFileName(settings.LogFile))) 
			in.WriteString(fmt.Sprintf("tar -czvf %v %v\n", target, tmp)) 
			in.WriteString(fmt.Sprintf("rm %v\n", tmp)) 
			in.WriteString("exit\n") 
		}() 
		if err := cmd.Run(); err != nil { 
			fmt.Println(err) 
			return err 
		} 
	} 
	return nil 
} 

基本的功能好像都能解决了,饱暖思淫欲,错误处理感觉用起来不怎么舒服,有更优雅的办法:

type Handler func(http.ResponseWriter, *http.Request) *models.APPError 
 
func (fn Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
	if e := fn(w, r); e != nil { 
		utils.Response(w, e.Message, e.Code, e.Status) 
	} 
 
} 
 
 
//装饰器就变成了这样 
func (inner Handler) Auth() Handler { 
	return Handler(func(w http.ResponseWriter, r *http.Request) *models.APPError { 
		tokenString := "" 
		cookie, _ := r.Cookie("token") 
		if cookie != nil { 
			tokenString = cookie.Value 
		} 
		if tokenString == "" { 
			if r.Header != nil { 
				if authorization := r.Header["Authorization"]; len(authorization) > 0 { 
					tokenString = authorization[0] 
				} 
			} 
		} 
		key, err := token.Valid(tokenString) 
		if err != nil { 
			return &models.APPError{err, "bad token.", "AUTH_FAILED", 403} 
		} 
		if !strings.Contains(key, "|") { 
			return &models.APPError{err, "user not found.", "NOT_FOUND", 404} 
		} 
		keys := strings.Split(key, "|") 
		rows, _, err := db.QueryNonLogging("select * from user where user_id = '%v' and user_pass = '%v'", keys[0], keys[1]) 
		if err != nil { 
			return &models.APPError{err, "can not connect database.", "DB_ERROR", 500} 
		} 
		if len(rows) == 0 { 
			return &models.APPError{err, "user not found.", "NOT_FOUND", 404} 
		} 
		go log.Printf("user_id:%v", keys[0]) 
		inner.ServeHTTP(w, r) 
		return nil 
	}) 
} 
//router画风也变了 
type Route struct { 
	Name        string 
	Method      string 
	Pattern     string 
	HandlerFunc Handler 
	ContentType string 
} 
 
type Routes []Route 
var BRoutes = Routes{ 
	Route{ 
		"nothing", 
		"GET", 
		"/", 
		Config, 
		contenttype.JSON, 
	}, 
	Route{ 
		"authDemo", 
		"GET", 
		"/demo1", 
		Handler(Config). 
			Auth(), 
		contenttype.JSON, 
	}, 
	Route{ 
		"verifyDemo", 
		"GET", 
		"/demo2", 
		Handler(Config). 
			Verify(), 
		contenttype.JSON, 
	}, 
	Route{ 
		"verifyAndAuthDemo", 
		"GET", 
		"/demo3", 
		Handler(Config). 
			Auth(). 
			Verify(), 
		contenttype.JSON, 
	}, 
} 

这样基本的web框架就完成了,想添加一些命令行工具,比如测试,自动生成app,推荐用kingpin来实现:

var ( 
	app      = kingpin.New("beauty", "A command-line tools of beauty.") 
	demo     = app.Command("demo", "Demo of web server.") 
	generate = app.Command("generate", "Generate a new app.") 
	name     = generate.Arg("name", "AppName for app.").Required().String() 
) 
 
func main() { 
	switch kingpin.MustParse(app.Parse(os.Args[1:])) { 
	case generate.FullCommand(): 
		GOPATH := os.Getenv("GOPATH") 
		appPath := fmt.Sprintf("%v/src/%v", GOPATH, *name) 
		origin := fmt.Sprintf("%v/src/github.com/yang-f/beauty/etc/demo.zip", GOPATH) 
		dst := fmt.Sprintf("%v.zip", appPath) 
		_, err := utils.CopyFile(dst, origin) 
		if err != nil { 
			fmt.Println(err.Error()) 
		} 
		utils.Unzip(dst, appPath) 
		os.RemoveAll(dst) 
		helper := utils.ReplaceHelper{ 
			Root:    appPath, 
			OldText: "{appName}", 
			NewText: *name, 
		} 
		helper.DoWrok() 
		log.Printf("Generate %s success.", *name) 
	case demo.FullCommand(): 
		log.Printf("Start server on port %s", settings.Listen) 
		router := router.NewRouter() 
		log.Fatal(http.ListenAndServe(settings.Listen, router)) 
	} 
 
} 

执行命令行是这样的:

usage: beauty [<flags>] <command> [<args> ...] 
 
A command-line tools of beauty. 
 
Flags: 
  --help  Show context-sensitive help (also try --help-long and --help-man). 
 
Commands: 
  help [<command>...] 
    Show help. 
 
  demo 
    Demo of web server. 
 
  generate <name> 
 
    Generate a new app. 

到此,这个框架还在不断的优化中,希望能有人提供宝贵的批评和建议。

以下是代码地址:

yang-f/beauty

谢谢!

发布评论

分享到:

IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

Faygo一款最适合开发API的 Go Web 框架详解
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。