Go/Grpc/kubernetes/Istio
Jenkins/Gitlab/HarborCICD
grpc-gateway

本最佳实践分为三个部分:

pingservicepongserviceJenkins/Gitlab/HarborCICDk8s/istio
pongservice

前提

假设已经安装了以下工具:

protoc-gen-grpc-gatewayprotoc-gen-openapiv2protoc-gen-goprotoc-gen-go-grpcbufwire

下面是安装这些工具的教程地址:

protoc-gen-grpc-gatewayprotoc-gen-openapiv2protoc-gen-goprotoc-gen-go-grpc
wirewire
bufbuf
项目结构

这部分最终的目录结构如下:

Jgrpc
├── devops
├── istio-manifests
├── kubernetes-manifests
└── src
    └── pongservice
        ├── buf.gen.yaml
        ├── cmd
        │   ├── main.go
        │   └── server
        │       ├── grpc.go
        │       ├── http.go
        │       ├── run.go
        │       ├── wire.go
        │       └── wire_gen.go
        ├── config
        │   ├── config.go
        │   └── config.yaml
        ├── genproto
        │   └── v1
        │       ├── gw
        │       │   └── pongservice.pb.gw.go
        │       ├── pongservice.pb.go
        │       └── pongservice_grpc.pb.go
        ├── go.mod
        ├── go.sum
        ├── proto
        │   ├── buf.lock
        │   ├── buf.yaml
        │   └── v1
        │       ├── pongservice.proto
        │       └── pongservice.yaml
        └── service
            ├── client.go
            └── server.go

14 directories, 20 files

开始

创建项目的整体目录结构如下:

Jgrpc
├── devops
├── istio-manifests
├── kubernetes-manifests
├── src
创建 pongservice 微服务

创建基本目录

srcpongservice
pongservice
├── cmd
│   └── server
├── config
├── proto
│   └── v1
└── service

6 directories, 0 files

生成代码和文件

生成 buf.yaml

在 pongservice/proto 目录下执行以下命令:

buf mod init
buf.yamlponservice/protobuf.yaml
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT
versionbreaking
deps:
  - buf.build/googleapis/googleapis
buf.yaml
version: v1
deps:
  - buf.build/googleapis/googleapis
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT
pongservice/proto
buf mod update
buf.lock
# Generated by buf. DO NOT EDIT.
version: v1
deps:
  - remote: buf.build
    owner: googleapis
    repository: googleapis
    commit: 463926e7ee924d46ad0a726e1cf4eacd

生成 pongservice.proto

pongservice/proto/v1pongservice.proto
syntax = "proto3";

package proto.v1;

option go_package = "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1";

service PongService {
  rpc Pong(PongRequest) returns(PongResponse){}
}

message PongRequest {
  string msg = 1 ;
}

message PongResponse {
  string msg = 1;
}

生成 pongservice.yaml

pongservice/proto/v1pongservice.yaml
type: google.api.Service
config_version: 3

http:
  rules:
    - selector: proto.v1.PongService.Pong
      get: /pong.v1.pong

生成 buf.gen.yaml

pongservicebuf.gen.yaml
version: v1
plugins:
  - plugin: go
    out: genproto/v1
    opt:
      - paths=source_relative
  - plugin: go-grpc
    out: genproto/v1
    opt:
      - paths=source_relative
  - plugin: grpc-gateway
    out: genproto/v1/gw
    opt:
      - paths=source_relative
      - grpc_api_configuration=proto/v1/pongservice.yaml
      - standalone=true
pongservice
buf generate proto/v1
pongservice
genproto
└── v1
    ├── gw
    │   └── ponservice.pb.gw.go
    ├── ponservice.pb.go
    └── ponservice_grpc.pb.go

2 directories, 3 files

生成 go.mod

pongservicego.mod
go mod init github.com/janrs-io/Jgrpc/src/pongservice && go mod tidy

生成 config.yaml

pongservice/configconfig.yaml
# grpc config
grpc:
  host: ""
  port: ":50051"
  name: "pong-grpc"

# http config
http:
  host: ""
  port: ":9001"
  name: "pong-http"

生成 config.go

pongservice/configconfig.go
package config

import (
    "net/http"

    "github.com/spf13/viper"
    "google.golang.org/grpc"
)

// Config Service config
type Config struct {
    Grpc Grpc `json:"grpc" yaml:"grpc"`
    Http Http `json:"http" yaml:"http"`
}

// NewConfig Initial service's config
func NewConfig(cfg string) *Config {

    if cfg == "" {
        panic("load config file failed.config file can not be empty.")
    }

    viper.SetConfigFile(cfg)

    // Read config file
    if err := viper.ReadInConfig(); err != nil {
        panic("read config failed.[ERROR]=>" + err.Error())
    }
    conf := &Config{}
    // Assign the overloaded configuration to the global
    if err := viper.Unmarshal(conf); err != nil {
        panic("assign config failed.[ERROR]=>" + err.Error())
    }

    return conf

}

// Grpc Grpc server config
type Grpc struct {
    Host   string `json:"host" yaml:"host"`
    Port   string `json:"port" yaml:"port"`
    Name   string `json:"name" yaml:"name"`
    Server *grpc.Server
}

// Http Http server config
type Http struct {
    Host   string `json:"host" yaml:"host"`
    Port   string `json:"port" yaml:"port"`
    Name   string `json:"name" yaml:"name"`
    Server *http.Server
}
pongservicego mod tidy

生成 client.go

pongservice/serviceclient.go
package service

import (
    "context"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    "github.com/janrs-io/Jgrpc/src/pongservice/config"
    v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// NewClient New service's client
func NewClient(conf *config.Config) (v1.PongServiceClient, error) {

    serverAddress := conf.Grpc.Host + conf.Grpc.Port
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    conn, err := grpc.DialContext(ctx, serverAddress, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return nil, err
    }
    client := v1.NewPongServiceClient(conn)
    return client, nil

}
pongservice/serviceserver.go
package service

import (
    "context"

    "github.com/janrs-io/Jgrpc/src/pongservice/config"
    v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// Server Server struct
type Server struct {
    v1.UnimplementedPongServiceServer
    pongClient v1.PongServiceClient
    conf       *config.Config
}

// NewServer New service grpc server
func NewServer(conf *config.Config, pongClient v1.PongServiceClient) v1.PongServiceServer {
    return &Server{
        pongClient: pongClient,
        conf:       conf,
    }
}

func (s *Server) Pong(ctx context.Context, req *v1.PongRequest) (*v1.PongResponse, error) {
    return &v1.PongResponse{Msg: "response pong msg:" + req.Msg}, nil
}

生成 run server 文件

pongservice/cmd/server
grpc.gohttp.gorun.gowire.go
grpc.go
package server

import (
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"

    "github.com/janrs-io/Jgrpc/src/pongservice/config"
    v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// RunGrpcServer Run grpc server
func RunGrpcServer(server v1.PongServiceServer, conf *config.Config) {

    grpcServer := grpc.NewServer()
    v1.RegisterPongServiceServer(grpcServer, server)

    fmt.Println("Listening grpc server on port" + conf.Grpc.Port)
    listen, err := net.Listen("tcp", conf.Grpc.Port)
    if err != nil {
        panic("listen grpc tcp failed.[ERROR]=>" + err.Error())
    }

    go func() {
        if err = grpcServer.Serve(listen); err != nil {
            log.Fatal("grpc serve failed", err)
        }
    }()

    conf.Grpc.Server = grpcServer

}
http.go
package server

import (
    "context"
    "fmt"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    "github.com/janrs-io/Jgrpc/src/pongservice/config"
    v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1/gw"
)

// RunHttpServer Run http server
func RunHttpServer(conf *config.Config) {

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    }

    if err := v1.RegisterPongServiceHandlerFromEndpoint(
        context.Background(),
        mux,
        conf.Grpc.Port,
        opts,
    ); err != nil {
        panic("register service handler failed.[ERROR]=>" + err.Error())
    }

    httpServer := &http.Server{
        Addr:    conf.Http.Port,
        Handler: mux,
    }
    fmt.Println("Listening http server on port" + conf.Http.Port)

    go func() {
        if err := httpServer.ListenAndServe(); err != nil {
            fmt.Println("listen http server failed.[ERROR]=>" + err.Error())
        }
    }()

    conf.Http.Server = httpServer

}
run.go
package server

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/janrs-io/Jgrpc/src/pongservice/config"
    v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
)

// Run Run service server
func Run(cfg string) {

    conf := config.NewConfig(cfg)
    // run grpc server
    RunGrpcServer(initServer(conf), conf)
    // run http server
    RunHttpServer(conf)
    // listen exit server event
    HandleExitServer(conf)

}

// SetServer Wire inject service's component
func initServer(conf *config.Config) v1.PongServiceServer {
    server, err := InitServer(conf)
    if err != nil {
        panic("run server failed.[ERROR]=>" + err.Error())
    }
    return server
}

// HandleExitServer Handle service exit event
func HandleExitServer(conf *config.Config) {

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
    <-ch
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    conf.Grpc.Server.GracefulStop()
    if err := conf.Http.Server.Shutdown(ctx); err != nil {
        panic("shutdown service failed.[ERROR]=>" + err.Error())
    }
    <-ctx.Done()
    close(ch)
    fmt.Println("Graceful shutdown http & grpc server.")

}
wire.go
//go:build wireinject
// +build wireinject

package server

import (
    "github.com/google/wire"

    "github.com/janrs-io/Jgrpc/src/pongservice/config"
    v1 "github.com/janrs-io/Jgrpc/src/pongservice/genproto/v1"
    "github.com/janrs-io/Jgrpc/src/pongservice/service"
)

// InitServer Inject service's component
func InitServer(conf *config.Config) (v1.PongServiceServer, error) {

    wire.Build(
        service.NewClient,
        service.NewServer,
    )

    return &service.Server{}, nil

}
pongservicego mod tidy
go mod tidy
pongservice
wire ./...
wirepongsevice/cmd/serverwire_gen.go

生成 main.go

pongservice/cmdmain.go
package main

import (
    "flag"

    "github.com/janrs-io/Jgrpc/src/pongservice/cmd/server"
)

var cfg = flag.String("config", "config/config.yaml", "config file location")

// main main
func main() {
    server.Run(*cfg)
}

启动 service

pongservice
pongservicepongservice/cmd
go run cmd/main.go

启动服务后,会显示如下信息:

Listening grpc server on port:50051
Listening http server on port:9001

在浏览器中输入以下地址即可访问该服务:

127.0.01:9001/pong.v1.pong?msg=best practice

如果成功,将返回以下数据:

{
    "msg": "response pong msg:best practice"
}
总结

现在,我们已经了解了如何创建可以开发基本功能微服务的项目结构。

pingservicepongservice