暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

go-web开发入坑指南

go技术沙龙 2021-09-08
704

点击 Golang入坑指南:传送门


简单API接口实现

    相信读过golang入坑指南读者应该对于go语言基础有了一定的了解,本篇文章则介绍一下go如何实现api接口,熟读这两篇文章后,相信大家直接上手go项目应该不成问题。

    另外本文依旧不会深入讲解Go Http源码级别的内容,关于源码后续会推出进阶系列的文章,本文的宗旨依旧是带领大家快速了解GO如何进行接口开发,先会用,在慢慢去了解其本质。

    Mux 是一个 Go 语言实现的Dispatcher
,用来处理各种 http 请求。

Hello World 实现方式一(HTTP版本的实现,非显示使用官方默认mux路由组件)
    package main


    import (
    "fmt"
    "log"
    "net/http"
    )


    const(
    host="127.0.0.1"
    port=9091
    )


    type HelloHandler struct {
    Name string `json:"name"`
    }


    func (h HelloHandler)ServeHTTP(w http.ResponseWriter,r *http.Request){
    if r.Method=="GET" {
    err := r.ParseForm()
    if err != nil {
    fmt.Println("parse form error")
    }
    name := r.FormValue("name")
    w.Write([]byte(fmt.Sprintf("你好:%v,我的名字是:%v", name, h.Name)))
    }else{
    w.Write([]byte("request method error"))
    }
    }


    func main(){
    fmt.Printf("start %v:%v\n",host,port)
    //方式一
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    if r.Method=="GET" {
    w.Write([]byte("Hello World"))
    }else{
    w.Write([]byte("Request Method Error"))
    }
    })


    //方式二 HelloHandler只要实现ServeHTTP即可
    http.Handle("/hello2",HelloHandler{Name: "张三"})


    if err := http.ListenAndServe(fmt.Sprintf("%v:%v",host,port), nil);err!=nil{
    fmt.Println(err)
    log.Fatal("服务运行失败")
    }
    }
    复制

    大家可以运行下上面的方法,感受一下golang启动Http服务的便捷,如果大家想深入了解Go 网络编程这块的原理,可以看下这篇文章传送门

    Hello World 实现方式二(显示使用官方默认mux路由组件)
      package main


      import (
      "context"
      "fmt"
      "net/http"
      "os"
      "os/signal"
      "syscall"
      "time"
      )


      func main(){


      //使用golang默认的路由组件
      defaultMux:=http.NewServeMux()
      defaultMux.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
      writer.Write([]byte("hello world"))
      })


      server:=http.Server{
      Addr: ":9091",
      Handler: defaultMux,
      ReadTimeout: 10*time.Second,
      WriteTimeout: 10*time.Second,
      IdleTimeout: 10*time.Second,
      }


      go func() {
      if err := server.ListenAndServe();err!=nil{
      fmt.Println("server listen error")
      }
      }()


      //平滑关闭
      ch:=make(chan os.Signal)
      signal.Notify(ch,syscall.SIGINT,syscall.SIGTERM)
      <-ch
      ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
      err := server.Shutdown(ctx)
      if err!=nil{
      cancel()
      }
      }
      复制

      Hello World 实现方式三(使用非官方开源mux路由组件)

      源码地址:gorilla/mux

        package main


        import (
        "context"
        "fmt"
        "github.com/gorilla/mux"
        "net/http"
        "os"
        "os/signal"
        "syscall"
        "time"
        )


        func main(){
        //使用grolla/mux
        r := mux.NewRouter()


        r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        writer.Write([]byte("hello"))
        })


        r.HandleFunc("/hello2", func(writer http.ResponseWriter, request *http.Request) {
        writer.Write([]byte("hello world2"))
        })


        server := http.Server{
        Addr: ":9091",
        Handler: r,
        ReadTimeout: 10 * time.Second * 10,
        WriteTimeout: 10 * time.Second * 10,
        IdleTimeout: 10 * time.Second * 10,
        }


        go func() {
        if err := server.ListenAndServe();err!=nil{
        fmt.Println("server listen error")
        }
        }()


        //平滑关闭
        ch:=make(chan os.Signal)
        signal.Notify(ch,syscall.SIGINT,syscall.SIGTERM)
        <-ch
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        err := server.Shutdown(ctx)
        if err!=nil{
        cancel()
        }
        }
        复制

        gorilla/mux对比golang自带的http.ServerMux

        http.ServerMux底层实现
          type ServeMux struct {
          mu sync.RWMutex
          m map[string]muxEntry
          es []muxEntry // slice of entries sorted from longest to shortest.
          hosts bool // whether any patterns contain hostnames
          }
          复制

          其实底层是一个map[string]muxEntry维护了路由信息,map中保存了请求路径和请求路径处理函数的一个映射。

          gorilla/mux底层实现

            type Router struct {
            NotFoundHandler http.Handler


            MethodNotAllowedHandler http.Handler


            routes []*Route


            // Routes by name for URL building.
            namedRoutes map[string]*Route


            KeepContext bool


            middlewares []middleware


            routeConf
            }
            复制

            其实这里分两种,第一种是routes []Route 切片实现,第二种是map[string]Route map实现,但是常用的是第一种。

            gorilla/mux优势
            • 支持正则路由

            • 支持按照method,header,host等信息匹配,方便实现restful风格的API接口

            • golang自带的路由只支持按照路径匹配

            • gorilla中有很对类库,一起配合使用完全可以满足工作需求

            其实在工作中身边朋友公司有很多直接用gorilla/mux来实现后端api接口,如果大家对于gorilla/mux感兴趣,可以自行百度尝试使用,后续也会推出gorilla/mux的深度使用介绍。

            Gin

            源码地址:gin

            • Gin 是一个用 Go (Golang) 编写的 web 框架。基于 httprouter实现路由功能,底层基于Radix树,占用内存少,性能高,速度提高了近 40 倍。

            • 支持中间件:传入的 HTTP 请求可以由一系列中间件和最终操作来处理。例如:Logger,Authorization,GZIP,最终操作 DB。

            • Crash 处理:Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!另外如果是新开的goroutine的,需要在新开的goroutine出recover住异常。

            • JSON 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。

            • 路由组:更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。

            • 错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。

            • 内置渲染:Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。

            • 可扩展性:可以自己新建中间件。

            好了这里不做对于gin的输入讲解,下面会通过一个登陆注册的例子,来向大家介绍一下项目中如何使用gin开发api接口。

            由于在golang中没有标准的web开发目录组织结构,因此大家可以参考我目前在用目录结构,也可以根据自己的经验设置

            项目架构

            数据层-Model
            user.go
              package userModel


              import (
              "codego/learngo/http_demo/login_demo/models/mysql"
              "errors"
              "github.com/jinzhu/gorm"
              )


              type User struct {
              Name string `json:"name"`
              Age int64 `json:"age"`
              Pwd string `json:"pwd"`
              }


              func NewUserDao()*User{
              return new(User)
              }


              func (u *User)GetUserByName(name string)(*User,error){
              user:=new(User)
              err := mysql.UserDB.Model(&User{}).Where("name=?", name).First(user).Error
              if err!=nil{
              if err==gorm.ErrRecordNotFound{
              return nil,errors.New("数据库记录不存在")
              }
              }
              return user,nil
              }


              func (u *User)GetUserById(id int64)(*User,error){
              user:=new(User)
              err := mysql.UserDB.Model(&User{}).Where("id=?", id).First(user).Error
              if err!=nil{
              if err==gorm.ErrRecordNotFound{
              return nil,errors.New("数据库记录不存在")
              }
              }
              return user,nil
              }




              func(u *User)InsertUser(user *User)error{
              return mysql.UserDB.Model(&User{}).Create(user).Error
              }
              复制

              主要完成对数据库的操作。

              db.go

                package mysql


                import "github.com/jinzhu/gorm"


                var UserDB *gorm.DB
                复制

                此处UserDB在setting.go中初始化数据库时赋值,这样在后续model中操作数据库即可直接使用。

                业务处理层-service

                user_service.go

                  package userService


                  import (
                  "codego/learngo/http_demo/login_demo/models/mysql/userModel"
                  "fmt"
                  )
                  type UserRegisterBo struct {
                  Name string `json:"name"` //用户名
                  Pwd string `json:"pwd"` //密码
                  Age int64 `json:"age"` //年龄
                  }


                  func GetUserById(id int64)*userModel.User{
                  userDao := userModel.NewUserDao()
                  user,err:=userDao.GetUserById(id)
                  if err!=nil{
                  //todo 记录日志
                  fmt.Println(err.Error())
                  return nil
                  }
                  return user
                  }


                  func GetUserByName(name string)*userModel.User{
                  userDao := userModel.NewUserDao()


                  info, err := userDao.GetUserByName(name)
                  if err!=nil{
                  //todo 记录日志
                  fmt.Println(err.Error())
                  return nil
                  }
                  return info
                  }


                  func UserInsert(bo *UserRegisterBo)bool{
                  userDao := userModel.NewUserDao()
                  user := &userModel.User{
                  Name: bo.Name,
                  Age: bo.Age,
                  Pwd: bo.Pwd,
                  }


                  if err := userDao.InsertUser(user);err!=nil{
                  //todo 记录日志
                  return false
                  }
                  return true
                  }
                  复制

                      业务逻辑的处理,同时要注意的一点是,golang的日志尽量都在一层完成,不要到处打日志,这样也不方便管理,这里没有详细介绍golang常用的日志组件,后续详细对比一下golang中经常用的日志组件,例如原生log,以及logrus,zap等等。

                  请求控制层-controller

                  userController.go

                    package userController


                    import (
                    "codego/learngo/http_demo/login_demo/app/services/userService"
                    "codego/learngo/http_demo/login_demo/utils"
                    "github.com/gin-gonic/gin"
                    "net/http"
                    "strconv"
                    )




                    type UserLoginBo struct {
                    Name string `json:"name"`
                    Pwd string `json:"pwd"`
                    }
                    //用户登陆
                    func Login(ctx *gin.Context){
                    bo:=new(UserLoginBo)
                    if err := ctx.BindJSON(bo);err!=nil{
                    ctx.JSON(http.StatusOK,gin.H{"data":nil,"error":"Invalid Params","code":utils.RequestFailed})
                    return
                    }


                    user := userService.GetUserByName(bo.Name)
                    if user==nil || user.Pwd!=bo.Pwd{
                    ctx.JSON(http.StatusOK,gin.H{"data":nil,"error":"用户名密码错误","code":utils.RequestFailed})
                    return
                    }
                    ctx.JSON(http.StatusOK,gin.H{"data":user,"error":"","code":utils.RequestSuccess})
                    }


                    //用户注册
                    func Register(ctx *gin.Context){
                    bo:=new(userService.UserRegisterBo)
                    if err:=ctx.BindJSON(bo);err!=nil{
                    ctx.JSON(http.StatusOK,gin.H{"data":nil,"error":"Invalid Params","code":utils.RequestFailed})
                    return
                    }


                    if userService.UserInsert(bo) {
                    ctx.JSON(http.StatusOK, gin.H{"data": "success", "error": "", "code": utils.RequestSuccess})
                    }else{


                    }
                    }


                    //获取用户信息-byName
                    func GetByName(ctx *gin.Context){
                    name:=ctx.Query("name")


                    userInfo := userService.GetUserByName(name)
                    if userInfo!=nil {
                    ctx.JSON(http.StatusOK, gin.H{"data": userInfo, "error": "", "code": utils.RequestSuccess})
                    }else{
                    ctx.JSON(http.StatusOK, gin.H{"data": nil, "error": "获取用户失败", "code": utils.RequestFailed})
                    }
                    }


                    //获取用户信息-byId
                    func GetById(ctx *gin.Context){
                    id,_:=strconv.Atoi(ctx.Query("id"))


                    userInfo := userService.GetUserById(int64(id))
                    if userInfo!=nil {
                    ctx.JSON(http.StatusOK, gin.H{"data": userInfo, "error": "", "code": utils.RequestSuccess})
                    }else{
                    ctx.JSON(http.StatusOK, gin.H{"data": nil, "error": "获取用户失败", "code": utils.RequestFailed})
                    }
                    }
                    复制

                    此处接收用户请求,获取请求参数,做转发等等。

                    路由转发--router

                    router.go

                      package routers


                      import "github.com/gin-gonic/gin"


                      func InitRouter() *gin.Engine{
                      r := gin.New()
                      r.Use(gin.Logger())
                      r.Use(gin.Recovery())
                      gin.SetMode("debug")
                      g:=r.Group("/v1/api")


                      //注册用户接口路由
                      InitUserRouter(g)


                      //r.Run(":9091")
                      return r
                      }
                      复制

                      userRouter.go

                        package routers


                        import (
                        "codego/learngo/http_demo/login_demo/app/controllers/userController"
                        "codego/learngo/http_demo/login_demo/app/middleware"
                        "github.com/gin-gonic/gin"
                        )


                        func InitUserRouter(g *gin.RouterGroup){
                        u:=g.Group("/user")
                        {
                        u.POST("/login",userController.Login)
                        u.POST("/register",userController.Register)
                        }


                        ui:=g.Group("/user",middleware.Jwt())
                        {
                        ui.GET("/getByName",userController.GetByName)
                        ui.GET("/getById",userController.GetById)
                        }
                        }
                        复制

                            这里涉及到了golang中路由模块,同时这里使用了路由分组的概念,方便路由的管理,后续会详细介绍,大家先模仿着写,先会用,在学原理。

                        中间件

                        jwt.go

                          package middleware


                          import (
                          "codego/learngo/http_demo/login_demo/utils"
                          "github.com/gin-gonic/gin"
                          "net/http"
                          )


                          func Jwt()gin.HandlerFunc{
                          return func(ctx *gin.Context) {
                          token := ctx.GetHeader("token")
                          //根据token校验用户
                          checkUser:= func(token string)int{
                          if token!=""{
                          //todo 此处校验用户Token
                          return utils.TokenSuccess
                          }
                          return utils.TokenError
                          }


                          if checkUser(token)==utils.TokenSuccess{
                          ctx.Next()
                          }else{
                          ctx.JSON(http.StatusOK,gin.H{"data":nil,"error":"Token Error","code":utils.TokenError})
                          ctx.Abort()
                          return
                          }
                          }
                          }
                          复制

                              大家可以看下这里jwt中间件的定义,在gin框架中用户可以根据自己的需要,定义类似jwt的通用中间件,例如日志,统计,限流等等,具体的详细其他复杂的后续在做介绍,大家先混个眼熟。

                          通用工具

                          utils.go

                            package utils


                            const (
                            RequestSuccess = iota
                            RequestFailed
                            TokenSuccess
                            TokenError
                            )
                            复制

                                这个包下主要定义一下平时经常使用的工具函数,这里定义了用于http请求的code。

                                这里说一下golang中的iota的用法,这里默认iota的值是0,依次递增 1,2,3,具体iota的多个变种用法也很少使用,这里不做详情介绍,后续推出详细文章介绍iota的使用。

                            配置文件

                            setting.go

                              package setting


                              import (
                              "codego/learngo/http_demo/login_demo/models/mysql"
                              "fmt"
                              "github.com/go-ini/ini"
                              "github.com/jinzhu/gorm"
                              _ "github.com/jinzhu/gorm/dialects/mysql"
                              "log"
                              "time"
                              )




                              var DBSetting =&DbConfig{}


                              type DbConfig struct {
                              Driver string `json:"driver"`
                              User string `json:"user"`
                              Password string `json:"password"`
                              Host string `json:"host"`
                              Name string `json:"name"`
                              }


                              //初始化
                              func init(){
                              //配置文件获取配置信息
                              configFile:= "config/app.ini"
                              cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, configFile)
                              if err != nil {
                              log.Fatalf("setting.Setup, fail to parse 'conf/app.ini': %v", err)
                              }


                              //获取数据库配置
                              cfg.Section("database").MapTo(DBSetting)


                              //todo 此处可以用于获取其他中间件配置
                              mysql.UserDB=InitDb(DBSetting.Driver,DBSetting.User,DBSetting.Password,DBSetting.Host,DBSetting.Name)
                              }


                              //初始化数据库
                              func InitDb(driver, user, password, host, name string) (db *gorm.DB) {
                              var err error
                              db, err = gorm.Open(driver, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local&timeout=10s",
                              user,
                              password,
                              host,
                              name))


                              if err!=nil{
                              log.Fatal("初始化数据库失败",err)
                              return nil
                              }


                              //打印日志
                              db.LogMode(true)
                              db.SingularTable(true)
                              db.DB().SetMaxIdleConns(10)
                              db.DB().SetMaxOpenConns(100)
                              db.DB().SetConnMaxLifetime(time.Second * 600)
                              return
                              }
                              复制

                              setting.go文件中做了两个工作:

                              • 读取配置文件

                              • 初始化数据库

                                  后续涉及到redis等其他中间件的初始化,都可以在这里做,另外需要注意的是,这里初始化数据库时,一定不要忘记引入包:_ "github.com/jinzhu/gorm/dialects/mysql" 这个包里面主要用于初始化mysql的驱动。

                                  在golang语言中引入包的时候如果前面用 “_” 标注,表示用于执行改包下的init函数,而不显示调用此包内的其他方法。

                              config.ini

                                [server]
                                #debug or release
                                RunMode = debug
                                HttpPort = 9091
                                ReadTimeout = 60
                                WriteTimeout = 60


                                [database]
                                Driver = mysql
                                User = root
                                Password = 123456
                                Host = 127.0.0.1:3306
                                Name = text
                                复制

                                运行结果

                                注册

                                登陆

                                获取用户详情

                                总结

                                    好了,到目前为止大家对于golang进行web开发有了一定的了解了,在通过go入坑指南这篇文章的基础加持,相信大家也可以开发自己的golang后端api项目了,另外这里没有介绍golang进行Html开发的模块,原因是在真正的项目开发中一般都是前后端分离项目,因此这里直接略过了html开发的知识。

                                    对于文章中没有深入讲解的内容,例如深入golang原生http源码,深入gin源码,gin中间件开发,以及日志模块等等,后续都会写专门的文章进行介绍,通过go入坑指南,go-web开发指南这两篇文章,旨在带领大家迈入go编程的世界,后续我会和大家一起学习,一起进步。

                                    最后希望大家持续关注风流倜傥小老头,持续关注go技术沙龙。

                                    点赞,在看,收藏,分享幺,感谢大家的认可与支持,有任何质疑大家都可留言。

                                    后续通过大家的建议与反馈,文章会一直编辑更新。

                                文章转载自go技术沙龙,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                评论