1. 程式人生 > >Golang構建簡單web框架

Golang構建簡單web框架

使用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

謝謝!