MixGo v1.1发布, Go 快速开发脚手架工具

Mix Go 是一个基于 Go 进行快速开发的完整系统,类似前端的 Vue CLI,提供:

  • 通过 mix-go/mixcli 实现的交互式项目脚手架:
    • 可以生成 cli, api, web, grpc 多种项目代码
    • 生成的代码开箱即用
    • 可选择是否需要 .env 环境配置
    • 可选择是否需要 .yml, .json, .toml 等独立配置
    • 可选择使用 gorm, xorm 的数据库
    • 可选择使用 logrus, zap 的日志库
  • 通过 mix-go/xcli 实现的命令行原型开发。
  • 基于 mix-go/xdi 的 DI, IoC 容器。

Github | Gitee

快速开始

安装

go get github.com/mix-go/mixcli

创建项目

$ mixcli new hello
Use the arrow keys to navigate: ↓ ↑ → ← 
? Select project type:
  ▸ CLI
    API
    Web (contains the websocket)
    gRPC

技术交流

知乎:https://www.zhihu.com/people/onanying
微博:http://weibo.com/onanying
官方QQ群:284806582, 825122875,敲门暗号:goer

编写一个 CLI 程序

首先我们使用 mixcli 命令创建一个项目骨架:

$ mixcli new hello
Use the arrow keys to navigate: ↓ ↑ → ← 
? Select project type:
  ▸ CLI
    API
    Web (contains the websocket)
    gRPC

生成骨架目录结构如下:

.
├── README.md
├── bin
├── commands
├── conf
├── configor
├── di
├── dotenv
├── go.mod
├── go.sum
├── logs
└── main.go

mian.go 文件:

  • xcli.AddCommand 方法传入的 commands.Commands 定义了全部的命令
package main

import (
    "github.com/mix-go/cli-skeleton/commands"
    _ "github.com/mix-go/cli-skeleton/configor"
    _ "github.com/mix-go/cli-skeleton/di"
    _ "github.com/mix-go/cli-skeleton/dotenv"
    "github.com/mix-go/dotenv"
    "github.com/mix-go/xcli"
)

func main() {
    xcli.SetName("app").
        SetVersion("0.0.0-alpha").
        SetDebug(dotenv.Getenv("APP_DEBUG").Bool(false))
    xcli.AddCommand(commands.Commands...).Run()
}

commands/main.go 文件:

我们可以在这里自定义命令,查看更多

  • RunI 定义了 hello 命令执行的接口,也可以使用 Run 设定一个匿名函数
package commands

import (
    "github.com/mix-go/xcli"
)

var Commands = []*xcli.Command{
    {
        Name:  "hello",
        Short: "\tEcho demo",
        Options: []*xcli.Option{
            {
                Names: []string{"n", "name"},
                Usage: "Your name",
            },
            {
                Names: []string{"say"},
                Usage: "\tSay ...",
            },
        },
        RunI: &HelloCommand{},
    },
}

commands/hello.go 文件:

业务代码写在 HelloCommand 结构体的 main 方法中

  • 代码中可以使用 flag 获取命令行参数,查看更多
package commands

import (
    "fmt"
    "github.com/mix-go/xcli/flag"
)

type HelloCommand struct {
}

func (t *HelloCommand) Main() {
    name := flag.Match("n", "name").String("OpenMix")
    say := flag.Match("say").String("Hello, World!")
    fmt.Printf("%s: %s\n", name, say)
}

接下来我们编译上面的程序:

  • linux & macOS
go build -o bin/go_build_main_go main.go
  • win
go build -o bin/go_build_main_go.exe main.go

查看全部命令的帮助信息:

$ cd bin
$ ./go_build_main_go 
Usage: ./go_build_main_go [OPTIONS] COMMAND [opt...]

Global Options:
  -h, --help    Print usage
  -v, --version Print version information

Commands:
  hello         Echo demo

Run './go_build_main_go COMMAND --help' for more information on a command.

Developed with Mix Go framework. (openmix.org/mix-go)

查看上面编写的 hello 命令的帮助信息:

$ ./go_build_main_go hello --help
Usage: ./go_build_main_go hello [opt...]

Command Options:
  -n, --name    Your name
  --say         Say ...

Developed with Mix Go framework. (openmix.org/mix-go)

执行 hello 命令,并传入两个参数:

$ ./go_build_main_go hello --name=liujian --say=hello
liujian: hello

编写一个 Worker Pool 队列消费

队列消费是高并发系统中最常用的异步处理模型,通常我们是编写一个 CLI 命令行程序在后台执行 Redis、RabbitMQ 等 MQ 的队列消费,并将处理结果落地到 mysql 等数据库中,由于这类需求的标准化比较容易,因此我们开发了 mix-go/xwp 库来处理这类需求,基本上大部分异步处理类需求都可使用。

新建 commands/workerpool.go 文件:

  • workerpool.NewDispatcher(jobQueue, 15, NewWorker) 创建了一个调度器
  • NewWorker 负责初始化执行任务的工作协程
  • 任务数据会在 worker.Do 方法中触发,我们只需要将我们的业务逻辑写到该方法中即可
  • 当程序接收到进程退出信号时,调度器能平滑控制所有的 Worker 在执行完队列里全部的任务后再退出调度,保证数据的完整性
package commands

import (
    "context"
    "fmt"
    "github.com/mix-go/cli-skeleton/di"
    "github.com/mix-go/xwp"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"
)

type worker struct {
    xwp.WorkerTrait
}

func (t *worker) Do(data interface{}) {
    defer func() {
        if err := recover(); err != nil {
            logger := di.Logrus()
            logger.Error(err)
        }
    }()

    // 执行业务处理
    // ...
    
    // 将处理结果落地到数据库
    // ...
}

func NewWorker() xwp.Worker {
    return &worker{}
}

type WorkerPoolDaemonCommand struct {
}

func (t *WorkerPoolDaemonCommand) Main() {
    redis := globals.Redis()
    jobQueue := make(chan interface{}, 50)
    d := xwp.NewDispatcher(jobQueue, 15, NewWorker)

    ch := make(chan os.Signal)
    signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-ch
        d.Stop()
    }()

    go func() {
        for {
            res, err := redis.BRPop(context.Background(), 3*time.Second, "foo").Result()
            if err != nil {
                if strings.Contains(err.Error(), "redis: nil") {
                    continue
                }
                fmt.Println(fmt.Sprintf("Redis Error: %s", err))
                d.Stop();
                return
            }
            // brPop命令最后一个键才是值
            jobQueue <- res[1]
        }
    }()

    d.Run() // 阻塞代码,直到任务全部执行完成并且全部 Worker 停止
}

接下来只需要把这个命令通过 xcli.AddCommand 注册到 CLI 中即可。

编写一个 API 服务

首先我们使用 mixcli 命令创建一个项目骨架:

$ mixcli new hello
Use the arrow keys to navigate: ↓ ↑ → ← 
? Select project type:
    CLI
  ▸ API
    Web (contains the websocket)
    gRPC

生成骨架目录结构如下:

.
├── README.md
├── bin
├── commands
├── conf
├── configor
├── controllers
├── di
├── dotenv
├── go.mod
├── go.sum
├── main.go
├── middleware
├── routes
└── runtime

mian.go 文件:

  • xcli.AddCommand 方法传入的 commands.Commands 定义了全部的命令
package main

import (
    "github.com/mix-go/api-skeleton/commands"
    _ "github.com/mix-go/api-skeleton/configor"
    _ "github.com/mix-go/api-skeleton/di"
    _ "github.com/mix-go/api-skeleton/dotenv"
    "github.com/mix-go/dotenv"
    "github.com/mix-go/xcli"
)

func main() {
    xcli.SetName("app").
        SetVersion("0.0.0-alpha").
        SetDebug(dotenv.Getenv("APP_DEBUG").Bool(false))
    xcli.AddCommand(commands.Commands...).Run()
}

commands/main.go 文件:

我们可以在这里自定义命令,查看更多

  • RunI 指定了命令执行的接口,也可以使用 Run 设定一个匿名函数
package commands

import (
    "github.com/mix-go/xcli"
)

var Commands = []*xcli.Command{
    {
        Name:  "api",
        Short: "\tStart the api server",
        Options: []*xcli.Option{
            {
                Names: []string{"a", "addr"},
                Usage: "\tListen to the specified address",
            },
            {
                Names: []string{"d", "daemon"},
                Usage: "\tRun in the background",
            },
        },
        RunI: &APICommand{},
    },
}

commands/api.go 文件:

业务代码写在 APICommand 结构体的 main 方法中,生成的代码中已经包含了:

  • 监听信号停止服务
  • 根据模式打印日志
  • 可选的后台守护执行

基本上无需修改即可上线使用

package commands

import (
    "context"
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/mix-go/api-skeleton/di"
    "github.com/mix-go/api-skeleton/routes"
    "github.com/mix-go/dotenv"
    "github.com/mix-go/xcli/flag"
    "github.com/mix-go/xcli/process"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"
)

type APICommand struct {
}

func (t *APICommand) Main() {
    if flag.Match("d", "daemon").Bool() {
        process.Daemon()
    }

    logger := di.Logrus()
    server := di.Server()
    addr := dotenv.Getenv("GIN_ADDR").String(":8080")
    mode := dotenv.Getenv("GIN_MODE").String(gin.ReleaseMode)

    // server
    gin.SetMode(mode)
    router := gin.New()
    routes.SetRoutes(router)
    server.Addr = flag.Match("a", "addr").String(addr)
    server.Handler = router

    // signal
    ch := make(chan os.Signal)
    signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-ch
        logger.Info("Server shutdown")
        ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
        if err := server.Shutdown(ctx); err != nil {
            logger.Errorf("Server shutdown error: %s", err)
        }
    }()

    // logger
    if mode != gin.ReleaseMode {
        handlerFunc := gin.LoggerWithConfig(gin.LoggerConfig{
            Formatter: func(params gin.LogFormatterParams) string {
                return fmt.Sprintf("%s|%s|%d|%s",
                    params.Method,
                    params.Path,
                    params.StatusCode,
                    params.ClientIP,
                )
            },
            Output: logger.Out,
        })
        router.Use(handlerFunc)
    }

    // run
    welcome()
    logger.Infof("Server start at %s", server.Addr)
    if err := server.ListenAndServe(); err != nil && !strings.Contains(err.Error(), "http: Server closed") {
        panic(err)
    }
}

routes/main.go 文件中配置路由:

已经包含一些常用实例,只需要在这里新增路由即可开始开发

package routes

import (
    "github.com/gin-gonic/gin"
    "github.com/mix-go/api-skeleton/controllers"
    "github.com/mix-go/api-skeleton/middleware"
)

func SetRoutes(router *gin.Engine) {
    router.Use(gin.Recovery()) // error handle

    router.GET("hello",
        middleware.CorsMiddleware(),
        func(ctx *gin.Context) {
            hello := controllers.HelloController{}
            hello.Index(ctx)
        },
    )

    router.POST("users/add",
        middleware.AuthMiddleware(),
        func(ctx *gin.Context) {
            hello := controllers.UserController{}
            hello.Add(ctx)
        },
    )

    router.POST("auth", func(ctx *gin.Context) {
        auth := controllers.AuthController{}
        auth.Index(ctx)
    })
}

接下来我们编译上面的程序:

  • linux & macOS
go build -o bin/go_build_main_go main.go
  • win
go build -o bin/go_build_main_go.exe main.go

启动服务器

$ bin/go_build_main_go api
             ___         
 ______ ___  _ /__ ___ _____ ______ 
  / __ `__ \/ /\ \/ /__  __ `/  __ \
 / / / / / / / /\ \/ _  /_/ // /_/ /
/_/ /_/ /_/_/ /_/\_\  \__, / \____/ 
                     /____/


Server      Name:      mix-api
Listen      Addr:      :8080
System      Name:      darwin
Go          Version:   1.13.4
Framework   Version:   1.0.9
time=2020-09-16 20:24:41.515 level=info msg=Server start file=api.go:58

编写一个 Web 服务

首先我们使用 mixcli 命令创建一个项目骨架:

$ mixcli new hello
Use the arrow keys to navigate: ↓ ↑ → ← 
? Select project type:
    CLI
    API
  ▸ Web (contains the websocket)
    gRPC

生成骨架目录结构如下:

.
├── README.md
├── bin
├── commands
├── conf
├── configor
├── controllers
├── di
├── dotenv
├── go.mod
├── go.sum
├── main.go
├── middleware
├── public
├── routes
├── runtime
└── templates

mian.go 文件:

  • xcli.AddCommand 方法传入的 commands.Commands 定义了全部的命令
package main

import (
    "github.com/mix-go/web-skeleton/commands"
    _ "github.com/mix-go/web-skeleton/configor"
    _ "github.com/mix-go/web-skeleton/di"
    _ "github.com/mix-go/web-skeleton/dotenv"
    "github.com/mix-go/dotenv"
    "github.com/mix-go/xcli"
)

func main() {
    xcli.SetName("app").
        SetVersion("0.0.0-alpha").
        SetDebug(dotenv.Getenv("APP_DEBUG").Bool(false))
    xcli.AddCommand(commands.Commands...).Run()
}

commands/main.go 文件:

我们可以在这里自定义命令,查看更多

  • RunI 指定了命令执行的接口,也可以使用 Run 设定一个匿名函数
package commands

import (
    "github.com/mix-go/xcli"
)

var Commands = []*xcli.Command{
    {
        Name:  "web",
        Short: "\tStart the web server",
        Options: []*xcli.Option{
            {
                Names: []string{"a", "addr"},
                Usage: "\tListen to the specified address",
            },
            {
                Names: []string{"d", "daemon"},
                Usage: "\tRun in the background",
            },
        },
        RunI: &WebCommand{},
    },
}

commands/web.go 文件:

业务代码写在 WebCommand 结构体的 main 方法中,生成的代码中已经包含了:

  • 监听信号停止服务
  • 根据模式打印日志
  • 可选的后台守护执行

基本上无需修改即可上线使用

package commands

import (
    "context"
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/mix-go/dotenv"
    "github.com/mix-go/web-skeleton/di"
    "github.com/mix-go/web-skeleton/routes"
    "github.com/mix-go/xcli"
    "github.com/mix-go/xcli/flag"
    "github.com/mix-go/xcli/process"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"
)

type WebCommand struct {
}

func (t *WebCommand) Main() {
    if flag.Match("d", "daemon").Bool() {
        process.Daemon()
    }

    logger := di.Logrus()
    server := di.Server()
    addr := dotenv.Getenv("GIN_ADDR").String(":8080")
    mode := dotenv.Getenv("GIN_MODE").String(gin.ReleaseMode)

    // server
    gin.SetMode(mode)
    router := gin.New()
    routes.SetRoutes(router)
    server.Addr = flag.Match("a", "addr").String(addr)
    server.Handler = router

    // signal
    ch := make(chan os.Signal)
    signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-ch
        logger.Info("Server shutdown")
        ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
        if err := server.Shutdown(ctx); err != nil {
            logger.Errorf("Server shutdown error: %s", err)
        }
    }()

    // logger
    if mode != gin.ReleaseMode {
        handlerFunc := gin.LoggerWithConfig(gin.LoggerConfig{
            Formatter: func(params gin.LogFormatterParams) string {
                return fmt.Sprintf("%s|%s|%d|%s",
                    params.Method,
                    params.Path,
                    params.StatusCode,
                    params.ClientIP,
                )
            },
            Output: logger.Out,
        })
        router.Use(handlerFunc)
    }

    // templates
    router.LoadHTMLGlob(fmt.Sprintf("%s/../templates/*", xcli.App().BasePath))

    // static file
    router.Static("/static", fmt.Sprintf("%s/../public/static", xcli.App().BasePath))
    router.StaticFile("/favicon.ico", fmt.Sprintf("%s/../public/favicon.ico", xcli.App().BasePath))

    // run
    welcome()
    logger.Infof("Server start at %s", server.Addr)
    if err := server.ListenAndServe(); err != nil && !strings.Contains(err.Error(), "http: Server closed") {
        panic(err)
    }
}

routes/main.go 文件中配置路由:

已经包含一些常用实例,只需要在这里新增路由即可开始开发

package routes

import (
    "github.com/gin-gonic/gin"
    "github.com/mix-go/web-skeleton/controllers"
    "github.com/mix-go/web-skeleton/middleware"
)

func SetRoutes(router *gin.Engine) {
    router.Use(gin.Recovery()) // error handle

    router.GET("hello",
        func(ctx *gin.Context) {
            hello := controllers.HelloController{}
            hello.Index(ctx)
        },
    )

    router.Any("users/add",
        middleware.SessionMiddleware(),
        func(ctx *gin.Context) {
            user := controllers.UserController{}
            user.Add(ctx)
        },
    )

    router.Any("login", func(ctx *gin.Context) {
        login := controllers.LoginController{}
        login.Index(ctx)
    })

    router.GET("websocket",
        func(ctx *gin.Context) {
            ws := controllers.WebSocketController{}
            ws.Index(ctx)
        },
    )
}

接下来我们编译上面的程序:

  • linux & macOS
go build -o bin/go_build_main_go main.go
  • win
go build -o bin/go_build_main_go.exe main.go

命令行启动 web 服务器:

$ bin/go_build_main_go web
             ___         
 ______ ___  _ /__ ___ _____ ______ 
  / __ `__ \/ /\ \/ /__  __ `/  __ \
 / / / / / / / /\ \/ _  /_/ // /_/ /
/_/ /_/ /_/_/ /_/\_\  \__, / \____/ 
                     /____/


Server      Name:      mix-web
Listen      Addr:      :8080
System      Name:      darwin
Go          Version:   1.13.4
Framework   Version:   1.0.9
time=2020-09-16 20:24:41.515 level=info msg=Server start file=web.go:58

浏览器测试:

编写一个 WebSocket 服务

WebSocket 是基于 http 协议完成握手的,因此我们编写代码时,也是和编写 Web 项目是差不多的,差别就是请求过来后,我们需要使用一个 WebSocket 的升级器,将请求升级为 WebSocket 连接,接下来就是针对连接的逻辑处理,从这个部分开始就和传统的 Socket 操作一致了。

routes/main.go 文件已经定义了一个 WebSocket 的路由:

router.GET("websocket",
    func(ctx *gin.Context) {
        ws := controllers.WebSocketController{}
        ws.Index(ctx)
    },
)

controllers/ws.go 文件:

  • 创建了一个 upgrader 的升级器,当请求过来时将会升级为 WebSocket 连接
  • 定义了一个 WebSocketSession 的结构体负责管理连接的整个生命周期
  • session.Start() 中启动了两个协程,分别处理消息的读和写
  • 在消息读取的协程中,启动了 WebSocketHandler 结构体的 Index 方法来处理消息,在实际项目中我们可以根据不同的消息内容使用不同的结构体来处理,实现 Web 项目那种控制器的功能
package controllers

import (
    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
    "github.com/mix-go/web-skeleton/di"
    "github.com/mix-go/xcli"
    "net/http"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

type WebSocketController struct {
}

func (t *WebSocketController) Index(c *gin.Context) {
    logger := di.Logrus()
    if xcli.App().Debug {
        upgrader.CheckOrigin = func(r *http.Request) bool {
            return true
        }
    }
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        logger.Error(err)
        c.Status(http.StatusInternalServerError)
        c.Abort()
        return
    }

    session := WebSocketSession{
        Conn:   conn,
        Header: c.Request.Header,
        Send:   make(chan []byte, 100),
    }
    session.Start()

    server := di.Server()
    server.RegisterOnShutdown(func() {
        session.Stop()
    })

    logger.Infof("Upgrade: %s", c.Request.UserAgent())
}

type WebSocketSession struct {
    Conn   *websocket.Conn
    Header http.Header
    Send   chan []byte
}

func (t *WebSocketSession) Start() {
    go func() {
        logger := di.Logrus()
        for {
            msgType, msg, err := t.Conn.ReadMessage()
            if err != nil {
                if !websocket.IsCloseError(err, 1001, 1006) {
                    logger.Error(err)
                }
                t.Stop()
                return
            }
            if msgType != websocket.TextMessage {
                continue
            }

            handler := WebSocketHandler{
                Session: t,
            }
            handler.Index(msg)
        }
    }()
    go func() {
        logger := di.Logrus()
        for {
            msg, ok := <-t.Send
            if !ok {
                return
            }
            if err := t.Conn.WriteMessage(websocket.TextMessage, msg); err != nil {
                logger.Error(err)
                t.Stop()
                return
            }
        }
    }()
}

func (t *WebSocketSession) Stop() {
    defer func() {
        if err := recover(); err != nil {
            logger := di.Logrus()
            logger.Error(err)
        }
    }()
    close(t.Send)
    _ = t.Conn.Close()
}

type WebSocketHandler struct {
    Session *WebSocketSession
}

func (t *WebSocketHandler) Index(msg []byte) {
    t.Session.Send <- []byte("hello, world!")
}

接下来我们编译上面的程序:

  • linux & macOS
go build -o bin/go_build_main_go main.go
  • win
go build -o bin/go_build_main_go.exe main.go

在命令行启动 web 服务器:

$ bin/go_build_main_go web
             ___         
 ______ ___  _ /__ ___ _____ ______ 
  / __ `__ \/ /\ \/ /__  __ `/  __ \
 / / / / / / / /\ \/ _  /_/ // /_/ /
/_/ /_/ /_/_/ /_/\_\  \__, / \____/ 
                     /____/


Server      Name:      mix-web
Listen      Addr:      :8080
System      Name:      darwin
Go          Version:   1.13.4
Framework   Version:   1.0.9
time=2020-09-16 20:24:41.515 level=info msg=Server start file=web.go:58

浏览器测试:

编写一个 gRPC 服务、客户端

首先我们使用 mixcli 命令创建一个项目骨架:

$ mixcli new hello
Use the arrow keys to navigate: ↓ ↑ → ← 
? Select project type:
    CLI
    API
    Web (contains the websocket)
  ▸ gRPC

生成骨架目录结构如下:

.
├── README.md
├── bin
├── commands
├── conf
├── configor
├── di
├── dotenv
├── go.mod
├── go.sum
├── main.go
├── protos
├── runtime
└── services

mian.go 文件:

  • xcli.AddCommand 方法传入的 commands.Commands 定义了全部的命令
package main

import (
    "github.com/mix-go/dotenv"
    "github.com/mix-go/grpc-skeleton/commands"
    _ "github.com/mix-go/grpc-skeleton/configor"
    _ "github.com/mix-go/grpc-skeleton/di"
    _ "github.com/mix-go/grpc-skeleton/dotenv"
    "github.com/mix-go/xcli"
)

func main() {
    xcli.SetName("app").
        SetVersion("0.0.0-alpha").
        SetDebug(dotenv.Getenv("APP_DEBUG").Bool(false))
    xcli.AddCommand(commands.Commands...).Run()
}

commands/main.go 文件:

我们可以在这里自定义命令,查看更多

  • 定义了 grpc:servergrpc:client 两个子命令
  • RunI 指定了命令执行的接口,也可以使用 Run 设定一个匿名函数
package commands

import (
    "github.com/mix-go/xcli"
)

var Commands = []*xcli.Command{
    {
        Name:  "grpc:server",
        Short: "gRPC server demo",
        Options: []*xcli.Option{
            {
                Names: []string{"d", "daemon"},
                Usage: "Run in the background",
            },
        },
        RunI: &GrpcServerCommand{},
    },
    {
        Name:  "grpc:client",
        Short: "gRPC client demo",
        RunI:  &GrpcClientCommand{},
    },
}

protos/user.proto 数据结构文件:

客户端与服务器端代码中都需要使用 .proto 生成的 go 代码,因为双方需要使用该数据结构通讯

  • .protogRPC 通信的数据结构文件,采用 protobuf 协议
syntax = "proto3";

package go.micro.grpc.user;
option go_package = ".;protos";

service User {
    rpc Add(AddRequest) returns (AddResponse) {}
}

message AddRequest {
    string Name = 1;
}

message AddResponse {
    int32 error_code = 1;
    string error_message = 2;
    int64 user_id = 3;
}

然后我们需要安装 gRPC 相关的编译程序:

接下来我们开始编译 .proto 文件:

  • 编译成功后会在当前目录生成 protos/user.pb.go 文件
cd protos
protoc --go_out=plugins=grpc:. user.proto

commands/server.go 文件:

服务端代码写在 GrpcServerCommand 结构体的 main 方法中,生成的代码中已经包含了:

  • 监听信号停止服务
  • 可选的后台守护执行
  • pb.RegisterUserServer 注册了一个默认服务,用户只需要扩展自己的服务即可
package commands

import (
    "github.com/mix-go/dotenv"
    "github.com/mix-go/grpc-skeleton/di"
    pb "github.com/mix-go/grpc-skeleton/protos"
    "github.com/mix-go/grpc-skeleton/services"
    "github.com/mix-go/xcli/flag"
    "github.com/mix-go/xcli/process"
    "google.golang.org/grpc"
    "net"
    "os"
    "os/signal"
    "strings"
    "syscall"
)

var listener net.Listener

type GrpcServerCommand struct {
}

func (t *GrpcServerCommand) Main() {
    if flag.Match("d", "daemon").Bool() {
        process.Daemon()
    }

    addr := dotenv.Getenv("GIN_ADDR").String(":8080")
    logger := di.Logrus()

    // listen
    listener, err := net.Listen("tcp", addr)
    if err != nil {
        panic(err)
    }
    listener = listener

    // signal
    ch := make(chan os.Signal)
    signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-ch
        logger.Info("Server shutdown")
        if err := listener.Close(); err != nil {
            panic(err)
        }
    }()

    // server
    s := grpc.NewServer()
    pb.RegisterUserServer(s, &services.UserService{})

    // run
    welcome()
    logger.Infof("Server run %s", addr)
    if err := s.Serve(listener); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
        panic(err)
    }
}

services/user.go 文件:

服务端代码中注册的 services.UserService{} 服务代码如下:

只需要填充业务逻辑即可

package services

import (
    "context"
    pb "github.com/mix-go/grpc-skeleton/protos"
)

type UserService struct {
}

func (t *UserService) Add(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) {
    // 执行数据库操作
    // ...

    resp := pb.AddResponse{
        ErrorCode:    0,
        ErrorMessage: "",
        UserId:       10001,
    }
    return &resp, nil
}

commands/client.go 文件:

客户端代码写在 GrpcClientCommand 结构体的 main 方法中,生成的代码中已经包含了:

  • 通过环境配置获取服务端连接地址
  • 设定了 5s 的执行超时时间
package commands

import (
    "context"
    "fmt"
    "github.com/mix-go/dotenv"
    pb "github.com/mix-go/grpc-skeleton/protos"
    "google.golang.org/grpc"
    "time"
)

type GrpcClientCommand struct {
}

func (t *GrpcClientCommand) Main() {
    addr := dotenv.Getenv("GIN_ADDR").String(":8080")
    ctx, _ := context.WithTimeout(context.Background(), time.Duration(5)*time.Second)
    conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        panic(err)
    }
    defer func() {
        _ = conn.Close()
    }()
    cli := pb.NewUserClient(conn)
    req := pb.AddRequest{
        Name: "xiaoliu",
    }
    resp, err := cli.Add(ctx, &req)
    if err != nil {
        panic(err)
    }
    fmt.Println(fmt.Sprintf("Add User: %d", resp.UserId))
}

接下来我们编译上面的程序:

  • linux & macOS
go build -o bin/go_build_main_go main.go
  • win
go build -o bin/go_build_main_go.exe main.go

首先在命令行启动 grpc:server 服务器:

$ bin/go_build_main_go grpc:server
             ___         
 ______ ___  _ /__ ___ _____ ______ 
  / __ `__ \/ /\ \/ /__  __ `/  __ \
 / / / / / / / /\ \/ _  /_/ // /_/ /
/_/ /_/ /_/_/ /_/\_\  \__, / \____/ 
                     /____/


Server      Name:      mix-grpc
Listen      Addr:      :8080
System      Name:      darwin
Go          Version:   1.13.4
Framework   Version:   1.0.20
time=2020-11-09 15:08:17.544 level=info msg=Server run :8080 file=server.go:46

然后开启一个新的终端,执行下面的客户端命令与上面的服务器通信

$ bin/go_build_main_go grpc:client
Add User: 10001

如何使用 DI 容器中的 Logger、Database、Redis 等组件

项目中要使用的公共组件,都定义在 di 目录,框架默认生成了一些常用的组件,用户也可以定义自己的组件,查看更多

  • 可以在哪里使用

可以在代码的任意位置使用,但是为了可以使用到环境变量和自定义配置,通常我们在 xcli.Command 结构体定义的 RunRunI 中使用。

  • 使用日志,比如:logruszap
logger := di.Logrus()
logger.Info("test")
  • 使用数据库,比如:gormxorm
db := di.Gorm()
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user)
fmt.Println(result)
  • 使用 Redis,比如:go-redis
rdb := di.GoRedis()
val, err := rdb.Get(context.Background(), "key").Result()
if err != nil {
    panic(err)
}
fmt.Println("key", val)

依赖

官方库

第三方库

License

Apache License Version 2.0, http://www.apache.org/licenses/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容