快速搭个golang微服务脚手架

321 阅读4分钟

项目结构

studysystem_micro
├─ cmd
│  └─ user
│  
├─ go.mod
├─ go.sum
├─ idl
│  ├─ user
│  │  ├─ user.pb.go
│  │  └─ user_grpc.pb.go
│  └─ user.proto
├─ internal
│  ├─ gateway
│  │  ├─ controller
│  │  │  ├─ api
│  │  │  │  └─ user
│  │  │  │     └─ user.go
│  │  │  └─ vo
│  │  │     └─ user.go
│  │  └─ route
│  │     └─ route.go
│  └─ service
│     └─ user
│        ├─ config
│        │  └─ gen.go
│        ├─ dao
│        │  ├─ email.go
│        │  └─ user.go
│        ├─ init.go
│        ├─ logs
│        ├─ model
│        │  └─ user.go
│        └─ service.go
├─ main.go
├─ pkg
│  ├─ init
│  │  ├─ config
│  │  │  └─ init.go
│  │  ├─ log
│  │  │  └─ log.go
│  │  ├─ serviceCenter
│  │  │  └─ consul.go
│  │  ├─ sql
│  │  │  ├─ mysql.go
│  │  │  ├─ pgsql.go
│  │  │  └─ redis.go
│  │  ├─ statuscode
│  │  │  └─ statuscode.go
│  │  └─ tracing
│  │     └─ tracing.go
│  └─ utils
│     ├─ encrypt.go
│     ├─ getlocalip.go
│     ├─ jwt.go
│     ├─ rand.go
│     └─ snowflake.go
├─ README.md
├─ rpc
│  ├─ config.go
│  ├─ initrpc.go
│  └─ user.go
└─ test
   ├─ configinit_test.go
   ├─ getwd_test.go
   └─ user_test.gob

cmd存储的是每个服务的启动主程序;idl存放proto文件以及根据proto文件生成的grpc调用的文件;internal存放每个服务的业务网关以及中间件;pkg存放的是需要的工具包,以及初始化的工具,rpc注册grpc的客户端.

配置中心

nacos作为注册中心

准备环境

   ###用docker安装nacos
   #下载镜像
   docker pull nacos/nacos-server
   #查看镜像
   docker images
   #运行容器
   docker run --env MODE=standalone --name mynacos -d -p 8848:8848 docker.io/nacos/nacos-server
   #查看启动日志,如果有successful字样就证明启动成功
   docker logs -f mynacos

连接nacos获取配置

//连接nacos
package config

import (
	"fmt"
	"log"
	"reflect"

	"github.com/nacos-group/nacos-sdk-go/clients"
	"github.com/nacos-group/nacos-sdk-go/common/constant"
	"github.com/nacos-group/nacos-sdk-go/vo"
)

const (
	nacos_Host = "xxx.xxx.x.xx"//你的nacos所在ip地址
	nacos_Port = 8848//nacos部署端口
)

// 每个服务初始化配置
func Init_config(data_id string, group string, value any) {//value结构初始化的结构体
	sc := []constant.ServerConfig{{
		IpAddr: nacos_Host,
		Port:   nacos_Port,
	}}

	cc := constant.ClientConfig{
		NamespaceId:         "232e51d6-1528-43b4-ab13-aa69039c7886", // 如果需要支持多namespace,我们可以场景多个client,它们有不同的NamespaceId。当namespace是public时,此处填空字符串。
		TimeoutMs:           5000,
		NotLoadCacheAtStart: true,
		LogDir:              "log",
		CacheDir:            "cache",
		LogLevel:            "debug",
	}

	configClient, err := clients.CreateConfigClient(map[string]interface{}{
		"serverConfigs": sc,
		"clientConfig":  cc,
	})
	if err != nil {
		fmt.Println(err.Error())
	}

	content, err := configClient.GetConfig(vo.ConfigParam{
		DataId: data_id,
		Group:  group,
	})
	SetConfig(content, value)

	if err != nil {
		fmt.Println(err.Error())
	}
	err = configClient.ListenConfig(vo.ConfigParam{
		DataId: data_id,
		Group:  group,
		OnChange: func(namespace, group, dataId, data string) {
			fmt.Println("配置文件发生了变化...")
			fmt.Println("group:" + group + ", dataId:" + dataId + ", data:" + data)
			SetConfig(data, value)
		},
	})
	if err != nil {
		fmt.Println(err.Error())
	}
}
//这里通过反射将配置数据绑定到结构体中
func SetConfig(content any, value any) {
	tval := reflect.TypeOf(value)
	if tval.Kind() != reflect.Ptr {
		log.Fatal("非指针类型")
		return
	}
	rval := reflect.ValueOf(value)
	val := reflect.ValueOf(content)
	rval.Method(0).Call([]reflect.Value{val})
}

测试案例

package test

import (
	"bytes"
	"fmt"
	"testing"

	"studysystem_micro/pkg/init/config"

	"github.com/spf13/viper"
)

type Test1 struct {
	Test `yaml:"test"`
}

type Test struct {
	A int `yaml:"a"`
}

func (a *Test1) BindData(c string) {
	fmt.Println(c)
	var runtime_viper = viper.New()
	runtime_viper.SetConfigType("yaml")
	runtime_viper.ReadConfig(bytes.NewBuffer([]byte(c)))
	runtime_viper.Unmarshal(a)
}
func TestXxx(t *testing.T) {
	v := new(Test1)
	config.Init_config("test", "DEFAULT_GROUP", v)
	fmt.Println(v)
}

注册中心

本项目用consul作为注册中心(用nacos也可以)

准备环境

   ###用docker安装consul
   #下载镜像
   docker pull consul
   #查看镜像
   docker images
   #运行容器
   docker run --name consul1 -d -p 8500:8500  consul agent -server -bootstrap-expect=1 -ui -bind=0.0.0.0 -client=0.0.0.0

连接consul注册服务和获取服务

package serviceCenter

import (
	"fmt"

	"github.com/hashicorp/consul/api"
)

// consul 定义一个consul结构体,其内部有一个`*api.Client`字段。
type consul struct {
	client *api.Client
}

// NewConsul 连接至consul服务返回一个consul对象
func NewConsul(addr string) (*consul, error) {
	cfg := api.DefaultConfig()
	cfg.Address = addr
	c, err := api.NewClient(cfg)
	if err != nil {
		return nil, err
	}
	return &consul{client: c}, nil
}

// RegisterService 将gRPC服务注册到consul
func (c *consul) RegisterService(serviceName string, ip string, port int) error {
	// 健康检查
	check := &api.AgentServiceCheck{
		GRPC:     fmt.Sprintf("%s:%d", ip, port), // 这里一定是外部可以访问的地址
		Timeout:  "10s",                          // 超时时间
		Interval: "10s",                          // 运行检查的频率
		// 指定时间后自动注销不健康的服务节点
		// 最小超时时间为1分钟,收获不健康服务的进程每30秒运行一次,因此触发注销的时间可能略长于配置的超时时间。
		DeregisterCriticalServiceAfter: "1m",
	}
	srv := &api.AgentServiceRegistration{
		ID:      fmt.Sprintf("%s-%s-%d", serviceName, ip, port), // 服务唯一ID
		Name:    serviceName,                                    // 服务名称
		Tags:    []string{"grpc", "consul"},                     // 为服务打标签
		Address: ip,
		Port:    port,
		Check:   check,
	}
	return c.client.Agent().ServiceRegister(srv)
}

// Deregister 注销服务
func (c *consul) Deregister(serviceName string, ip string, port int) error {
	return c.client.Agent().ServiceDeregister(fmt.Sprintf("%s-%s-%d", serviceName, ip, port))
}

客户端连接

package test

import (
	"context"
	"fmt"
	pb "studysystem_micro/idl/user"
	"testing"

	grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
	_ "github.com/mbobakov/grpc-consul-resolver"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func TestUser(t *testing.T) {
	conn, err := grpc.Dial("consul://xxx.xxx.x.xx:8500/user?healthy=true", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`), grpc.WithChainUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor()))
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	client := pb.NewUserClient(conn)
	v, _ := client.GetAuthCode(context.Background(), &pb.UserGetAuthCode{
		Email: "2745969694@qq.com",
	})
	fmt.Println(v)
}

链路追踪

package tracing

import (
	"io"
	"os"

	opentracing "github.com/opentracing/opentracing-go"
	jaeger "github.com/uber/jaeger-client-go"
	con "github.com/uber/jaeger-client-go/config"
)
//服务名称,jaeger所在主机ip,端口
func InitTracer(service string, host string, port string) (opentracing.Tracer, io.Closer) {
	os.Setenv("JAEGER_AGENT_HOST", host)
	os.Setenv("JAEGER_AGENT_PORT", port)
	cfg, err := con.FromEnv()
	if err != nil {
		panic(err)
	}

	cfg.ServiceName = service
	cfg.Sampler.Type = "const"
	cfg.Sampler.Param = 1
	cfg.Reporter.LogSpans = true

	tracer, closer, err := cfg.NewTracer(con.Logger(jaeger.StdLogger))
	if err != nil {
		panic(err)
	}
	return tracer, closer
}
//服务注册添加如下代码
	tracer, closer := tracing.InitTracer(gen.C.Server.Name, gen.C.Tracing.Host, gen.C.Tracing.Port)
	defer closer.Close()
	opentracing.SetGlobalTracer(tracer)

网关

    //这边用gin作为微服务的统一网关,用法跟正常单体的web项目差不多
    import "github.com/gin-gonic/gin"

数据库(不止数据库)

//用gorm,像redis,rebbitmq这些中间件就用就不一一介绍了
package sql

import (
	"fmt"
	"reflect"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var db *gorm.DB

func GetDB() *gorm.DB {
	return db
}
//这里用反射将结构体的数据提取出来
func InitMysql(val any) {
	t := reflect.TypeOf(val)
	if t.Kind() != reflect.Struct {
		fmt.Println("非结构体类型")
		return
	}
	v := reflect.ValueOf(val)
	dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", v.Field(0).String(), v.Field(1).String(), v.Field(2).String(), v.Field(3).String(), v.Field(4).String())
	var err error
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		DisableForeignKeyConstraintWhenMigrating: true,
	})
	if err != nil {
		fmt.Println(err)
	}
}

后面还有细节啥的,比如上框架,中间件这些,后面有时间再补,如果看完觉得有用的话麻烦帮小编点个赞啦

下面是我用这个项目结构写的一个项目,可以拿来参考参考,麻烦看官顺便点个star呦👉 项目参考链接