Agenda
    拨开云雾见天日:前置知识讲解
    万丈高楼平地起:基础中的精髓 及 搭建企业级golang脚手架
    秤砣虽小压千斤:实战学习开发用户管理系统
    拨开云雾见天日
    1-1 前置知识
    Go开发web的优势
    在 Go 语言出现之前,开发者们总是面临非常艰难的抉择,
    究竟是使用执行速度快但是编译速度并不理想的语言(如:C++),还是
    使用编译速度较快但执行效率不佳的语言(如:.NET、Java),
    开发难度较低但执行速度一般的动态语言呢?(如:Python)
    显然,Go 语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发。
    image.png
    gin是什么?
    Gin 框架 现在是 github 上 start 最多 Go 语言编写的 Web 框架,相比其他它的几个 start 数量差不多的框架,它更加的轻量,有更好的性能。
    性能对比
    image.png
    在固定时间内重复完成的总数,数值越高的是越好的结果
    单次重复持续时间(ns /op),越低越好
    堆内存(B /op),越低越好
    每次重复的平均分配(allocs /op),越低越好
    从内存分配、单词相应时间、Qps三个纬度分析。gin基本是碾压其他对手的。
    httprouter 为gin插上了翅膀
    image.png
    https://chai2010.gitbooks.io/advanced-go-programming-book/ch5-web/ch5-02-router.html
    https://www.cnblogs.com/foxy/p/9469401.html
    1-2 golang开发环境准备

    1. 下载与解压
    2. wget https://dl.google.com/go/go1.11.4.linux-amd64.tar.gz
    3. tar -xvf go1.11.4.linux-amd64.tar.gz
    4. 修改环境变量
    5. vim ~/.bash_profile
    6. export GOROOT=/root/go
    7. export GOPATH=/root/go_path
    8. export GOBIN=$GOROOT/bin
    9. export PATH=$GOBIN:$GOROOT/bin:$GOPATH/bin:$PATH
    10. source ~/.bash_profile
    11. 查看go的版本号
    12. go version

    1-2 go mod 包管理工具
    https://blog.csdn.net/e421083458/article/details/89762113
    万丈高楼平地起
    2-1 安装gin及快速开始
    测试使用版本:
    github.com/gin-gonic/gin v1.4.0

    1. go get -v github.com/gin-gonic/gin
    2. package main
    3. import "github.com/gin-gonic/gin"
    4. func main() {
    5. r := gin.Default()
    6. r.GET("/ping", func(c *gin.Context) {
    7. c.JSON(200, gin.H{
    8. "message": "pong",
    9. })
    10. })
    11. r.Run() // listen and serve on 0.0.0.0:8080
    12. }

    2-2 请求路由
    设置多种请求类型

    1. func main() {
    2. // Creates a gin router with default middleware:
    3. // logger and recovery (crash-free) middleware
    4. router := gin.Default()
    5. router.GET("/someGet", getting)
    6. router.POST("/somePost", posting)
    7. router.PUT("/somePut", putting)
    8. router.DELETE("/someDelete", deleting)
    9. router.PATCH("/somePatch", patching)
    10. router.HEAD("/someHead", head)
    11. router.OPTIONS("/someOptions", options)
    12. // By default it serves on :8080 unless a
    13. // PORT environment variable was defined.
    14. router.Run()
    15. // router.Run(":3000") for a hard coded port
    16. }

    绑定静态文件夹

    1. func main() {
    2. router := gin.Default()
    3. router.Static("/assets", "./assets")
    4. router.StaticFS("/more_static", http.Dir("my_file_system"))
    5. router.StaticFile("/favicon.ico", "./resources/favicon.ico")
    6. // Listen and serve on 0.0.0.0:8080
    7. router.Run(":8080")
    8. }

    参数作为URL

    1. package main
    2. import "github.com/gin-gonic/gin"
    3. type Person struct {
    4. ID string `uri:"id" binding:"required,uuid"`
    5. Name string `uri:"name" binding:"required"`
    6. }
    7. func main() {
    8. route := gin.Default()
    9. route.GET("/:name/:id", func(c *gin.Context) {
    10. var person Person
    11. if err := c.ShouldBindUri(&person); err != nil {
    12. c.JSON(400, gin.H{"msg": err})
    13. return
    14. }
    15. c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
    16. })
    17. route.Run(":8088")
    18. }

    正则绑定

    1. package main
    2. import (
    3. "github.com/gin-gonic/gin"
    4. "net/http"
    5. )
    6. func main() {
    7. router := gin.Default()
    8. router.GET("/user/*action", func(c *gin.Context) {
    9. c.String(http.StatusOK, "Hello world")
    10. })
    11. router.Run(":8080")
    12. }

    2-3 获取请求参数
    获取GET请求参数

    1. func main() {
    2. router := gin.Default()
    3. // Query string parameters are parsed using the existing underlying request object.
    4. // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe
    5. router.GET("/welcome", func(c *gin.Context) {
    6. firstname := c.DefaultQuery("firstname", "Guest")
    7. lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
    8. c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    9. })
    10. router.Run(":8080")
    11. }

    获取POST请求参数

    1. func main() {
    2. router := gin.Default()
    3. router.POST("/form_post", func(c *gin.Context) {
    4. message := c.PostForm("message")
    5. nick := c.DefaultPostForm("nick", "anonymous")
    6. c.JSON(200, gin.H{
    7. "status": "posted",
    8. "message": message,
    9. "nick": nick,
    10. })
    11. })
    12. router.Run(":8080")
    13. }

    获取Body值

    1. package main
    2. import (
    3. "github.com/gin-gonic/gin"
    4. "io/ioutil"
    5. "net/http"
    6. )
    7. func main() {
    8. router := gin.Default()
    9. router.POST("/user/*action", func(c *gin.Context) {
    10. bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
    11. c.String(http.StatusOK, string(bodyBytes))
    12. })
    13. router.Run(":8080")
    14. }

    测试效果:

    1. #form-urlencode
    2. curl 'http://127.0.0.1:8080/user/adsadsad' -F "key=value"
    3. --------------------------88baf4eac8fd4ba1
    4. Content-Disposition: form-data; name="key"
    5. value
    6. --------------------------88baf4eac8fd4ba1--
    7. #form-data
    8. curl 'http://127.0.0.1:8080/user/adsadsad' -d "key=value"
    9. key=value

    bind绑定结构体获取参数(Get&POST&POST_BODY)
    只需要在结构体上设置form标签即可

    1. package main
    2. import (
    3. "fmt"
    4. "log"
    5. "time"
    6. "github.com/gin-gonic/gin"
    7. )
    8. type Person struct {
    9. Name string `form:"name"`
    10. Address string `form:"address"`
    11. Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
    12. }
    13. func main() {
    14. route := gin.Default()
    15. route.GET("/testing", startPage)
    16. route.POST("/testing", startPage)
    17. route.Run(":8085")
    18. }
    19. func startPage(c *gin.Context) {
    20. var person Person
    21. if c.ShouldBind(&person) == nil {
    22. log.Println(person.Name)
    23. log.Println(person.Address)
    24. log.Println(person.Birthday)
    25. }
    26. c.String(200, fmt.Sprintf("%#v",person))
    27. }

    测试

    1. curl 'http://127.0.0.1:8085/testing?name=1&address=2&birthday=2006-01-02'
    2. main.Person{Name:"1", Address:"2", Birthday:time.Time{wall:0x0, ext:63271756800, loc:(*time.Location)(nil)}}
    3. curl 'http://127.0.0.1:8085/testing' -d 'name=5566&address=3344&birthday=2006-01-02'
    4. main.Person{Name:"5566", Address:"3344", Birthday:time.Time{wall:0x0, ext:63271756800, loc:(*time.Location)(nil)}}%

    2-4 验证请求参数
    结构体binding验证
    gin默认使用validator.v8作为验证器。
    多种验证规则请参见:
    https://github.com/go-playground/validator/tree/v8
    https://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Using_Validator_Tags
    注意点:gin必须使用 binding tag 来设置校验规则,而不是 validate。

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. "github.com/gin-gonic/gin"
    6. )
    7. type Person struct {
    8. Name string `form:"name" binding:"required"`
    9. Address string `form:"address"`
    10. Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
    11. }
    12. func main() {
    13. route := gin.Default()
    14. route.GET("/testing", startPage)
    15. route.Run(":8085")
    16. }
    17. func startPage(c *gin.Context) {
    18. var person Person
    19. if err:=c.ShouldBind(&person);err!=nil {
    20. c.String(500,fmt.Sprint(err))
    21. }
    22. c.String(200, fmt.Sprintf("%#v",person))
    23. }

    测试

    1. curl 'http://127.0.0.1:8085/testing' -d 'name=&address=3344&birthday=2006-01-02&age=1'
    2. Key: 'Person.Age' Error:Field validation for 'Age' failed on the 'gt' tag
    3. Key: 'Person.Name' Error:Field validation for 'Name' failed on the 'required' tagmain.Person{Age:1, Name:"", Address:"3344", Birthday:time.Time{wall:0x0, ext:63271756800, loc:(*time.Location)(nil)}}
    4. curl 'http://127.0.0.1:8085/testing' -d 'name=5566&address=3344&birthday=2006-01-02&age=1'
    5. Key: 'Person.Age' Error:Field validation for 'Age' failed on the 'gt' tagmain.Person{Age:1, Name:"5566", Address:"3344", Birthday:time.Time{wall:0x0, ext:63271756800, loc:(*time.Location)(nil)}}

    自定义验证

    1. package main
    2. import (
    3. "net/http"
    4. "reflect"
    5. "time"
    6. "github.com/gin-gonic/gin"
    7. "github.com/gin-gonic/gin/binding"
    8. "gopkg.in/go-playground/validator.v8"
    9. )
    10. // Booking contains binded and validated data.
    11. type Booking struct {
    12. CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
    13. CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
    14. }
    15. func bookableDate(
    16. v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
    17. field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
    18. ) bool {
    19. if date, ok := field.Interface().(time.Time); ok {
    20. today := time.Now()
    21. if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
    22. return false
    23. }
    24. }
    25. return true
    26. }
    27. func main() {
    28. route := gin.Default()
    29. if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    30. v.RegisterValidation("bookabledate", bookableDate)
    31. }
    32. route.GET("/bookable", getBookable)
    33. route.Run(":8085")
    34. }
    35. func getBookable(c *gin.Context) {
    36. var b Booking
    37. if err := c.ShouldBindWith(&b, binding.Query); err == nil {
    38. c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
    39. } else {
    40. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    41. }
    42. }
    1. $ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
    2. {"message":"Booking dates are valid!"}
    3. $ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
    4. {"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}

    升级v9验证,支持多语言错误信息
    注意v9验证的话,标签要使用 validate ,也是为了与v8的标签分开。

    1. package main
    2. import (
    3. "fmt"
    4. "github.com/gin-gonic/gin"
    5. "github.com/go-playground/locales/en"
    6. "github.com/go-playground/locales/zh"
    7. "github.com/go-playground/locales/zh_Hant_TW"
    8. "github.com/go-playground/universal-translator"
    9. "gopkg.in/go-playground/validator.v9"
    10. zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
    11. en_translations "gopkg.in/go-playground/validator.v9/translations/en"
    12. zh_tw_translations "gopkg.in/go-playground/validator.v9/translations/zh_tw"
    13. )
    14. var (
    15. Uni *ut.UniversalTranslator
    16. Validate *validator.Validate
    17. )
    18. type User struct {
    19. Username string `form:"user_name" validate:"required"`
    20. Tagline string `form:"tag_line" validate:"required,lt=10"`
    21. Tagline2 string `form:"tag_line2" validate:"required,gt=1"`
    22. }
    23. func main() {
    24. en := en.New()
    25. zh := zh.New()
    26. zh_tw := zh_Hant_TW.New()
    27. Uni = ut.New(en, zh, zh_tw)
    28. Validate = validator.New()
    29. route := gin.Default()
    30. route.GET("/testing", startPage)
    31. route.POST("/testing", startPage)
    32. route.Run(":8085")
    33. }
    34. func startPage(c *gin.Context) {
    35. //这部分应该放到中间件中
    36. locale:=c.DefaultQuery("locale","zh")
    37. trans, _ := Uni.GetTranslator(locale)
    38. switch locale {
    39. case "zh":
    40. zh_translations.RegisterDefaultTranslations(Validate, trans)
    41. break
    42. case "en":
    43. en_translations.RegisterDefaultTranslations(Validate, trans)
    44. break
    45. case "zh_tw":
    46. zh_tw_translations.RegisterDefaultTranslations(Validate, trans)
    47. break
    48. default:
    49. zh_translations.RegisterDefaultTranslations(Validate, trans)
    50. break
    51. }
    52. //自定义错误内容
    53. Validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
    54. return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
    55. }, func(ut ut.Translator, fe validator.FieldError) string {
    56. t, _ := ut.T("required", fe.Field())
    57. return t
    58. })
    59. //这块应该放到公共验证方法中
    60. user := User{}
    61. c.ShouldBind(&user)
    62. fmt.Println(user)
    63. err := Validate.Struct(user)
    64. if err != nil {
    65. errs := err.(validator.ValidationErrors)
    66. sliceErrs:=[]string{}
    67. for _, e := range errs {
    68. sliceErrs=append(sliceErrs,e.Translate(trans))
    69. }
    70. c.String(200,fmt.Sprintf("%#v",sliceErrs))
    71. }
    72. c.String(200, fmt.Sprintf("%#v",""))
    73. }

    测试

    1. http://127.0.0.1:8085/testing?user_name=1111111&tag_line=1111122222222222222222&tag_line=111&locale=zh
    2. []string{"Tagline长度必须小于10个字符", "Tagline2 must have a value!"}""
    3. http://127.0.0.1:8085/testing?user_name=1111111&tag_line=1111122222222222222222&tag_line=111&locale=zh
    4. []string{"Tagline must be less than 10 characters in length", "Tagline2 must have a value!"}""

    2-5 中间件
    使用gin中间件

    1. func Logger() gin.HandlerFunc {
    2. return func(c *gin.Context) {
    3. t := time.Now()
    4. // Set example variable
    5. c.Set("example", "12345")
    6. // before request
    7. c.Next()
    8. // after request
    9. latency := time.Since(t)
    10. log.Print(latency)
    11. // access the status we are sending
    12. status := c.Writer.Status()
    13. log.Println(status)
    14. }
    15. }
    16. func main() {
    17. r := gin.New()
    18. r.Use(Logger())
    19. r.GET("/test", func(c *gin.Context) {
    20. example := c.MustGet("example").(string)
    21. // it would print: "12345"
    22. log.Println(example)
    23. })
    24. // Listen and serve on 0.0.0.0:8080
    25. r.Run(":8080")
    26. }

    自定义ip白名单中间件

    1. package main
    2. import (
    3. "fmt"
    4. "github.com/gin-gonic/gin"
    5. "log"
    6. )
    7. func IPAuthMiddleware() gin.HandlerFunc {
    8. return func(c *gin.Context) {
    9. ipList:=[]string{
    10. "192.168.1.1",
    11. }
    12. isMatched:=false
    13. for _, host := range ipList {
    14. if c.ClientIP() == host {
    15. isMatched = true
    16. }
    17. }
    18. if !isMatched{
    19. c.String(200,fmt.Sprintf("%v, not in iplist", c.ClientIP()))
    20. c.Abort()
    21. }
    22. }
    23. }
    24. func main() {
    25. r := gin.New()
    26. r.Use(IPAuthMiddleware())
    27. r.GET("/test", func(c *gin.Context) {
    28. example := c.MustGet("example").(string)
    29. log.Println(example)
    30. })
    31. r.Run(":8080")
    32. }

    2-6 其他补充
    优雅关停
    确保在 go1.8+ 下编译

    1. // +build go1.8
    2. package main
    3. import (
    4. "context"
    5. "log"
    6. "net/http"
    7. "os"
    8. "os/signal"
    9. "syscall"
    10. "time"
    11. "github.com/gin-gonic/gin"
    12. )
    13. func main() {
    14. router := gin.Default()
    15. router.GET("/", func(c *gin.Context) {
    16. time.Sleep(5 * time.Second)
    17. c.String(http.StatusOK, "Welcome Gin Server")
    18. })
    19. srv := &http.Server{
    20. Addr: ":8080",
    21. Handler: router,
    22. }
    23. go func() {
    24. // service connections
    25. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
    26. log.Fatalf("listen: %s\n", err)
    27. }
    28. }()
    29. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    30. <-quit
    31. log.Println("Shutdown Server ...")
    32. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    33. defer cancel()
    34. if err := srv.Shutdown(ctx); err != nil {
    35. log.Fatal("Server Shutdown:", err)
    36. }
    37. select {
    38. case <-ctx.Done():
    39. log.Println("timeout of 5 seconds.")
    40. }
    41. log.Println("Server exiting")
    42. }

    Linux Signal及Golang中的信号处理
    https://blog.csdn.net/leonpengweicn/article/details/52131313

    模板渲染
    gin在html/template基础之上,进行的文件解析预处理。模块文件使用方法与之前相同。
    LoadHTMLGlob 可以解析一个文件夹的模板。

    1. package main
    2. import (
    3. "github.com/gin-gonic/gin"
    4. "net/http"
    5. )
    6. func main() {
    7. router := gin.Default()
    8. //router.LoadHTMLFiles("templates/index.html",)
    9. router.LoadHTMLGlob("templates/*")
    10. router.GET("/index", func(c *gin.Context) {
    11. c.HTML(http.StatusOK, "index.html", gin.H{
    12. "title": "Main website",
    13. })
    14. })
    15. router.Run(":8080")
    16. }

    index.html

    1. <html>
    2. <h1>
    3. {{ .title }}
    4. </h1>
    5. </html>

    template使用样例
    https://www.do1618.com/archives/893/go-template学习样例/
    https://www.calhoun.io/intro-to-templates-p3-functions/

    Let’s Encrypt 自动证书下载
    无需配置任何证书即可自动下载证书,过期后自动续订。
    Let’s Encrypt原理
    image.png
    更多原理细节:
    https://www.cnblogs.com/esofar/p/9291685.html
    https://blog.csdn.net/canghaiguzhou/article/details/79945001
    gin使用方法超级简单:

    1. package main
    2. import (
    3. "log"
    4. "github.com/gin-gonic/autotls"
    5. "github.com/gin-gonic/gin"
    6. )
    7. func main() {
    8. r := gin.Default()
    9. r.GET("/ping", func(c *gin.Context) {
    10. c.String(200, "pong")
    11. })
    12. log.Fatal(autotls.Run(r, "www.itpp.tk"))
    13. }

    测试

    1. https://www.itpp.tk/ping
    2. pong

    查看证书缓存

    1. [root@izrj998xcgvke9018fkvbvz autotls]# ls ~/.cache/golang-autocert/
    2. acme_account+key www.itpp.tk

    2-7 构建企业级脚手架
    代码地址:https://github.com/e421083458/gin_scaffold
    使用gin构建了企业级脚手架,代码简洁易读,可快速进行高效web开发。
    主要功能有:
    1、请求链路日志打印,涵盖mysql/redis/request_in/request_out
    2、接入validator.v9,支持多语言错误信息提示及自定义错误提示。
    3、借助golang_common,支持了多配置环境及log/redis/mysql/http.client
    现在开始
    安装软件依赖
    go mod使用请查阅:

    1. go mod tidy

    运行脚本

    1. go run main.go
    2. gin_scaffold git:(master) go run main.go
    3. ------------------------------------------------------------------------
    4. [INFO] config=./conf/dev/
    5. [INFO] start loading resources.
    6. [INFO] success loading resources.
    7. ------------------------------------------------------------------------
    8. [GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
    9. [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
    10. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
    11. - using env: export GIN_MODE=release
    12. - using code: gin.SetMode(gin.ReleaseMode)
    13. [GIN-debug] GET /demo/index --> github.com/e421083458/gin_scaffold/controller.(*Demo).Index-fm (6 handlers)
    14. [GIN-debug] GET /demo/bind --> github.com/e421083458/gin_scaffold/controller.(*Demo).Bind-fm (6 handlers)
    15. [GIN-debug] GET /demo/dao --> github.com/e421083458/gin_scaffold/controller.(*Demo).Dao-fm (6 handlers)
    16. [GIN-debug] GET /demo/redis --> github.com/e421083458/gin_scaffold/controller.(*Demo).Redis-fm (6 handlers)
    17. [INFO] HttpServerRun::8880

    测试mysql与请求链路

    1. curl 'http://127.0.0.1:8880/demo/dao?id=1'
    2. {
    3. "errno": 0,
    4. "errmsg": "",
    5. "data": "[{\"id\":1,\"area_name\":\"area_name\",\"city_id\":1,\"user_id\":2,\"update_at\":\"2019-06-15T00:00:00+08:00\",\"create_at\":\"2019-06-15T00:00:00+08:00\",\"delete_at\":\"2019-06-15T00:00:00+08:00\"}]",
    6. "trace_id": "c0a8fe445d05b9eeee780f9f5a8581b0"
    7. }
    8. 查看链路日志(确认是不是一次请求查询,都带有相同trace_id):
    9. tail -f gin_scaffold.inf.log
    10. [INFO][2019-06-16T11:39:26.802][log.go:58] _com_request_in||method=GET||from=127.0.0.1||traceid=c0a8fe445d05b9eeee780f9f5a8581b0||cspanid=||uri=/demo/dao?id=1||args=map[]||body=||spanid=9dad47aa57e9d186
    11. [INFO][2019-06-16T11:39:26.802][log.go:58] _com_mysql_success||affected_row=1||traceid=c0a8fe445d05b9ee07b80f9f66cb39b0||spanid=9dad47aa1408d2ac||source=/Users/niuyufu/go/src/github.com/e421083458/gin_scaffold/dao/demo.go:24||proc_time=0.000000000||sql=SELECT * FROM `area` WHERE (id = '1')||level=sql||current_time=2019-06-16 11:39:26||cspanid=
    12. [INFO][2019-06-16T11:39:26.802][log.go:58] _com_request_out||method=GET||args=map[]||proc_time=0.025019164||traceid=c0a8fe445d05b9eeee780f9f5a8581b0||spanid=9dad47aa57e9d186||uri=/demo/dao?id=1||from=127.0.0.1||response={\"errno\":0,\"errmsg\":\"\",\"data\":\"[{\\\"id\\\":1,\\\"area_name\\\":\\\"area_name\\\",\\\"city_id\\\":1,\\\"user_id\\\":2,\\\"update_at\\\":\\\"2019-06-15T00:00:00+08:00\\\",\\\"create_at\\\":\\\"2019-06-15T00:00:00+08:00\\\",\\\"delete_at\\\":\\\"2019-06-15T00:00:00+08:00\\\"}]\",\"trace_id\":\"c0a8fe445d05b9eeee780f9f5a8581b0\"}||cspanid=

    测试参数绑定与多语言验证

    1. curl 'http://127.0.0.1:8880/demo/bind?name=name&locale=zh'
    2. {
    3. "errno": 500,
    4. "errmsg": "Age为必填字段,Passwd为必填字段",
    5. "data": "",
    6. "trace_id": "c0a8fe445d05badae8c00f9fb62158b0"
    7. }
    8. curl 'http://127.0.0.1:8880/demo/bind?name=name&locale=en'
    9. {
    10. "errno": 500,
    11. "errmsg": "Age is a required field,Passwd is a required field",
    12. "data": "",
    13. "trace_id": "c0a8fe445d05bb4cd3b00f9f3a768bb0"
    14. }

    文件分层

    1. ├── README.md
    2. ├── conf 配置文件夹
    3. └── dev
    4. ├── base.toml
    5. ├── mysql_map.toml
    6. └── redis_map.toml
    7. ├── controller 控制器
    8. └── demo.go
    9. ├── dao DB数据访问层
    10. └── demo.go
    11. ├── dto Bind结构体层
    12. └── demo.go
    13. ├── gin_scaffold.inf.log info日志
    14. ├── gin_scaffold.wf.log warning日志
    15. ├── go.mod go module管理文件
    16. ├── go.sum
    17. ├── main.go
    18. ├── middleware 中间件层
    19. ├── panic.go
    20. ├── response.go
    21. ├── token_auth.go
    22. └── translation.go
    23. ├── public 公共文件
    24. ├── log.go
    25. ├── mysql.go
    26. └── validate.go
    27. ├── router 路由层
    28. ├── httpserver.go
    29. └── route.go
    30. └── tmpl

    引入轻量级golang类库,支持 mysql、redis、http.client、log、支持多级环境配置、支持链路日志打印

    1. go get -v github.com/e421083458/golang_common

    测试日志打印功能:

    1. package main
    2. import (
    3. "github.com/e421083458/golang_common/lib"
    4. "log"
    5. "time"
    6. )
    7. func main() {
    8. if err := lib.InitModule("./conf/dev/",[]string{"base","mysql","redis",}); err != nil {
    9. log.Fatal(err)
    10. }
    11. defer lib.Destroy()
    12. //todo sth
    13. lib.Log.TagInfo(lib.NewTrace(), lib.DLTagUndefind, map[string]interface{}{"message": "todo sth"})
    14. time.Sleep(time.Second)
    15. }

    类库更多功能细节请查看:
    https://github.com/e421083458/golang_common
    输出格式统一封装

    1. func ResponseError(c *gin.Context, code ResponseCode, err error) {
    2. trace, _ := c.Get("trace")
    3. traceContext, _ := trace.(*lib.TraceContext)
    4. traceId := ""
    5. if traceContext != nil {
    6. traceId = traceContext.TraceId
    7. }
    8. resp := &Response{ErrorCode: code, ErrorMsg: err.Error(), Data: "", TraceId: traceId}
    9. c.JSON(200, resp)
    10. response, _ := json.Marshal(resp)
    11. c.Set("response", string(response))
    12. c.AbortWithError(200, err)
    13. }
    14. func ResponseSuccess(c *gin.Context, data interface{}) {
    15. trace, _ := c.Get("trace")
    16. traceContext, _ := trace.(*lib.TraceContext)
    17. traceId := ""
    18. if traceContext != nil {
    19. traceId = traceContext.TraceId
    20. }
    21. resp := &Response{ErrorCode: SuccessCode, ErrorMsg: "", Data: data, TraceId: traceId}
    22. c.JSON(200, resp)
    23. response, _ := json.Marshal(resp)
    24. c.Set("response", string(response))
    25. }

    定义中间件链路日志打印

    1. package middleware
    2. import (
    3. "bytes"
    4. "errors"
    5. "fmt"
    6. "github.com/e421083458/gin_scaffold/public"
    7. "github.com/e421083458/golang_common/lib"
    8. "github.com/gin-gonic/gin"
    9. "io/ioutil"
    10. "time"
    11. )
    12. //链路请求日志
    13. func RequestInLog(c *gin.Context) {
    14. traceContext := lib.NewTrace()
    15. if traceId := c.Request.Header.Get("com-header-rid"); traceId != "" {
    16. traceContext.TraceId = traceId
    17. }
    18. if spanId := c.Request.Header.Get("com-header-spanid"); spanId != "" {
    19. traceContext.SpanId = spanId
    20. }
    21. c.Set("startExecTime", time.Now())
    22. c.Set("trace", traceContext)
    23. bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
    24. c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) // Write body back
    25. lib.Log.TagInfo(traceContext, "_com_request_in", map[string]interface{}{
    26. "uri": c.Request.RequestURI,
    27. "method": c.Request.Method,
    28. "args": c.Request.PostForm,
    29. "body": string(bodyBytes),
    30. "from": c.ClientIP(),
    31. })
    32. }
    33. //链路输出日志
    34. func RequestOutLog(c *gin.Context) {
    35. endExecTime := time.Now()
    36. response, _ := c.Get("response")
    37. st, _ := c.Get("startExecTime")
    38. startExecTime, _ := st.(time.Time)
    39. public.ComLogNotice(c, "_com_request_out", map[string]interface{}{
    40. "uri": c.Request.RequestURI,
    41. "method": c.Request.Method,
    42. "args": c.Request.PostForm,
    43. "from": c.ClientIP(),
    44. "response": response,
    45. "proc_time": endExecTime.Sub(startExecTime).Seconds(),
    46. })
    47. }
    48. func TokenAuthMiddleware() gin.HandlerFunc {
    49. return func(c *gin.Context) {
    50. RequestInLog(c)
    51. defer RequestOutLog(c)
    52. isMatched := false
    53. for _, host := range lib.GetStringSliceConf("base.http.allow_ip") {
    54. if c.ClientIP() == host {
    55. isMatched = true
    56. }
    57. }
    58. if !isMatched{
    59. ResponseError(c, InternalErrorCode, errors.New(fmt.Sprintf("%v, not in iplist", c.ClientIP())))
    60. c.Abort()
    61. return
    62. }
    63. c.Next()
    64. }
    65. }

    请求数据绑定到结构体与校验
    dto/demo.go

    1. package dto
    2. import (
    3. "errors"
    4. "github.com/e421083458/gin_scaffold/public"
    5. "github.com/gin-gonic/gin"
    6. "github.com/go-playground/universal-translator"
    7. "gopkg.in/go-playground/validator.v9"
    8. "strings"
    9. )
    10. type InStruct struct {
    11. Name string `form:"name" validate:"required"`
    12. Age int64 `form:"age" validate:"required"`
    13. Passwd string `form:"passwd" validate:"required"`
    14. }
    15. func (o *InStruct) BindingValidParams(c *gin.Context) error{
    16. if err:=c.ShouldBind(o);err!=nil{
    17. return err
    18. }
    19. v:=c.Value("trans")
    20. trans,ok := v.(ut.Translator)
    21. if !ok{
    22. trans, _ = public.Uni.GetTranslator("zh")
    23. }
    24. err := public.Validate.Struct(o)
    25. if err != nil {
    26. errs := err.(validator.ValidationErrors)
    27. sliceErrs:=[]string{}
    28. for _, e := range errs {
    29. sliceErrs=append(sliceErrs,e.Translate(trans))
    30. }
    31. return errors.New(strings.Join(sliceErrs,","))
    32. }
    33. return nil
    34. }

    controller/demo.go

    1. func (demo *Demo) Bind(c *gin.Context) {
    2. st:=&dto.InStruct{}
    3. if err:=st.BindingValidParams(c);err!=nil{
    4. middleware.ResponseError(c,500,err)
    5. return
    6. }
    7. middleware.ResponseSuccess(c, "")
    8. return
    9. }

    秤砣虽小压千斤
    3-1 用户管理系统设计
    image.png
    image.png
    功能点
    管理员登陆及退出
    用户列表 翻页
    增、删、改
    数据库
    用户表设计

    1. CREATE TABLE `user` (
    2. `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
    3. `name` varchar(255) NOT NULL DEFAULT '' COMMENT '姓名',
    4. `addr` varchar(255) NOT NULL DEFAULT '' COMMENT '住址',
    5. `age` smallint(4) NOT NULL DEFAULT '0' COMMENT '年龄',
    6. `birth` varchar(100) NOT NULL DEFAULT '2000-01-01 00:00:00' COMMENT '生日',
    7. `sex` smallint(4) NOT NULL DEFAULT '0' COMMENT '性别',
    8. `update_at` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '更新时间',
    9. `create_at` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间',
    10. PRIMARY KEY (`id`)
    11. ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8 COMMENT='用户表'

    后端准备
    fork一份gin脚手架程序
    https://github.com/e421083458/gin_scaffold
    前端准备
    下载vue-admin
    https://github.com/taylorchen709/vue-admin
    安装及设置webstorm
    调整webstorm的vue.js插件
    https://www.cnblogs.com/ssrsblogs/p/6231981.html
    调整webstorm的es6
    https://blog.csdn.net/qijuju/article/details/79015073
    3-2 实战开发
    开发接口

    1. [GIN-debug] POST /api/login --> github.com/e421083458/gin_scaffold/controller.(*Api).Login-fm (7 handlers)
    2. [GIN-debug] GET /api/loginout --> github.com/e421083458/gin_scaffold/controller.(*Api).LoginOut-fm (7 handlers)
    3. [GIN-debug] GET /api/user/listpage --> github.com/e421083458/gin_scaffold/controller.(*Api).ListPage-fm (8 handlers)
    4. [GIN-debug] GET /api/user/add --> github.com/e421083458/gin_scaffold/controller.(*Api).AddUser-fm (8 handlers)
    5. [GIN-debug] GET /api/user/edit --> github.com/e421083458/gin_scaffold/controller.(*Api).EditUser-fm (8 handlers)
    6. [GIN-debug] GET /api/user/remove --> github.com/e421083458/gin_scaffold/controller.(*Api).RemoveUser-fm (8 handlers)
    7. [GIN-debug] GET /api/user/batchremove --> github.com/e421083458/gin_scaffold/controller.(*Api).RemoveUser-fm (8 handlers)

    整合前端调试

    1. npm run dev

    修改 config/index.js文件,作接口代理转发

    1. dev: {
    2. env: require('./dev.env'),
    3. port: 8080,
    4. autoOpenBrowser: true,
    5. assetsSubDirectory: 'static',
    6. assetsPublicPath: '/',
    7. proxyTable: {
    8. "/api/": {
    9. target:"http://127.0.0.1:8880/",
    10. changeOrigin: true,
    11. pathRewrite: {
    12. }
    13. }
    14. },
    15. cssSourceMap: false
    16. }

    前后端部署与整合
    正式部署时我们一般考虑使用nginx 作转发。

    1. server {
    2. listen 8882;
    3. server_name localhost;
    4. root /Users/niuyufu/WebstormProjects/vue-admin/dist;
    5. index index.html index.htm index.php;
    6. location / {
    7. try_files $uri $uri/ /index.html?$args;
    8. }
    9. location /api/ {
    10. proxy_pass http://127.0.0.1:8880/api/;
    11. }
    12. }

    课程总结
    随堂笔记
    https://blog.csdn.net/e421083458/article/details/91994788
    gin入门实战 - 基础精髓 随堂代码
    https://github.com/e421083458/hello_gin
    golang基础代码类库
    https://github.com/e421083458/golang_common
    gin开发脚手架
    https://github.com/e421083458/gin_scaffold
    整合vue.js的admin后台
    https://github.com/e421083458/vue-admin
    fork一份gin脚手架程序
    https://github.com/e421083458/gin_scaffold
    前端准备
    下载vue-admin
    https://github.com/taylorchen709/vue-admin
    安装及设置webstorm
    调整webstorm的vue.js插件
    https://www.cnblogs.com/ssrsblogs/p/6231981.html
    调整webstorm的es6
    https://blog.csdn.net/qijuju/article/details/79015073