一个文件夹内有多个go程序文件怎样打开_go-micro V2 从零开始(二)手写第一个微服务

86e8c7f2d23675bef9a0b606b480c73c.png

本文相关代码:gitee


前言

上一章我们借助micro工具包创建了一个demo程序hello-service,并通过编写hello-cli.go成功实现了微服务调用。

接下来的若干章我们参考示例代码,编写一组自己的微服务,并逐步引入第三方插件,最终编写一个微服务版的todolist程序.

这一章,我们先来手写第一个微服务:task-srv

作为一个todolist服务,最基础的功能当然是对任务的增删改查等管理操作,考虑到实际业务复杂度,这里我们选择引入mongodb作为系统数据库保存task信息


具体步骤

一、创建目录

首先创建项目总的目录,这里我们起名为go-todolist

mkdir go-todolist && cd go-todolist
# 初始化go-todolist为go mod项目
go mod init go-todolist

# 继续创建`task-srv`目录
mkdir task-srv && cd task-srv

# 这部分可以参考上一章的官方demo程序
# 注意这里多出一个repository文件夹用于数据库相关操作
mkdir proto repository handler 
cd proto
mkdir task

此服务只提供gRpc一种调用方式,不订阅消息,因此会发现我们并没有创建subscriber文件夹。


二、编写task.proto

新建并编辑task-srv/proto/task/task.proto,这里我们首先定义了task对象的增、删、改和分页查询接口。

需要注意的是,消息体定义使用了限定修饰符,限定修饰符包含requiredoptionalrepeated

  • Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字 段的值,对于接收方,必须能够识别该字段的意思。 ~~这个修饰符在proto3中已废弃~~
  • Optional: 表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。 对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。 这个修饰符在proto3中是所有字段的默认修饰符
  • Repeated: 表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。
//声明proto本版
syntax = "proto3";
//服务名
package go.micro.service.task;
//生成go文件的包路径
option go_package = "proto/task";

//定义task服务的接口,主要是增删改查
//结构非常类似于go语言的interface定义,只是返回值必须用括号包裹,且不能使用基本类型作为参数或返回值
service TaskService {
  rpc Create(Task)returns (EditResponse){}
  rpc Delete(Task)returns (EditResponse){}
  rpc Modify(Task)returns (EditResponse){}
  rpc Finished(Task)returns (EditResponse){}
  rpc Search(SearchRequest)returns (SearchResponse){}
}

//下面是消息体message的定义,可以暂时理解为go中的struct,其中的1,2,3...是每个变量唯一的编码
message Task {
  //每条任务的ID,本项目中对应mongodb记录的"_id"字段
  //@inject_tag: bson:"_id"
  string id = 1;
  //任务主体文字
  //@inject_tag: bson:"body"
  string body = 2;
  //用户设定的任务开始时间戳
  //@inject_tag: bson:"startTime"
  int64 startTime = 3;
  //用户设定的任务截止时间戳
  //@inject_tag: bson:"endTime"
  int64 endTime = 4;
  //任务是否已完成
  //@inject_tag: bson:"isFinished"
  int32 isFinished = 5;
  //用户实际完成时间戳
  //@inject_tag: bson:"finishTime"
  int64 finishTime = 6;
  //任务创建时间
  //@inject_tag: bson:"createTime"
  int64 createTime = 7;
  //任务修改时间
  //@inject_tag: bson:"updateTime"
  int64 updateTime = 8;
}

//增删改接口返回参数
message EditResponse {
  //操作返回的消息
  string msg = 1;
}
//查询接口的参数
message SearchRequest{
  //分页查询页码,从第一页开始
  int64 pageSize = 1;
  //分页查询每页数量,默认20
  int64 pageCode = 2;
  // 排序字段
  string sortBy = 3;
  // 顺序 -1降序 1升序
  int32 order=4;
  //关键字模糊查询任务body字段
  string keyword = 5;
}

message SearchResponse{
  //分页查询页码,从第一页开始
  int64 pageSize = 1;
  //分页查询每页数量,默认20
  int64 pageCode = 2;
  // 排序字段
  string sortBy = 3;
  // 顺序 -1降序 1升序
  int32 order=4;
  //数据总数
  int64 total = 5;
  //具体数据,这里repeated表示可以出现多条,类似于go中的slice
  repeated Task rows = 6;
}

细心的朋友会发现,在proto文件中,除了必要的注释外还有一些类似java注解的注释@inject_tag,这是由于protobuf官方的插件不能很好的处理go struct的tag内容,而我们的数据库mongodb又需要在tag中配置bson信息,否则生成的struct无法正确接收_id,另外生成的表字段也会被全部小写不符合阅读习惯。 这里需要引入一个第三方的自动生成工具:

go get -u github.com/favadi/protoc-go-inject-tag

他的配置很简单,只需要在需要设置tag的字段上写类似//@inject_tag bson:"_id"这样的注解即可。 当然,要使用这个工具,也需要调用额外的命令,其中input参数的值是生成后go文件的相对路径。

protoc-go-inject-tag -input=proto/task/task.pb.go

为了便于使用,这里我们参考上一章demo中的makefile文件,在task目录下也创建可以makefile文件:

GOPATH:=$(shell go env GOPATH)
MODIFY=Mproto/imports/api.proto=github.com/micro/go-micro/v2/api/proto

.PHONY: proto
proto:

    protoc --proto_path=. --micro_out=${MODIFY}:. --go_out=${MODIFY}:. proto/task/task.proto
    # 注意这里我们添加了tag控件的命令
    protoc-go-inject-tag -input=proto/task/task.pb.go

.PHONY: build
build: proto

    go build -o task-srv main.go

.PHONY: test
test:
    go test -v ./... -cover

.PHONY: docker
docker:
    docker build . -t task-srv:latest

接下来,在task-srv文件夹下运行make proto即可以生成proto的go文件。

没有make命令的win用户也可以在task-srv目录下直接执行命令:

> protoc --proto_path=. --micro_out=. --go_out=. proto/task/task.proto
> protoc-go-inject-tag -input=proto/task/task.pb.go

这个时候你的两个go文件一定是满屏飘红的,因为缺少依赖,进入task-srv目录,执行go mod tidy下载基础依赖,因为之前demo项目已经下载过相关依赖,这个命令执行会很快。


三、实现服务

用编辑器打开task.pb.micro.go,我们首先能看到,protoc已经帮我生成了go语言版的TaskService服务相关代码,根据注释,我们需要实现接口TaskServiceHandler:

...

// Server API for TaskService service

type TaskService interface {
    Create(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)
    Delete(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)
    Modify(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)
    Finished(ctx context.Context, in *Task, opts ...client.CallOption) (*EditResponse, error)
    Search(ctx context.Context, in *SearchRequest, opts ...client.CallOption) (*SearchResponse, error)
}

...

你会注意到,除了我们自己定义的 *Task参数外,go-micro还自动封装了上下文context.Context,原本我们定义的返回值EditResponse变成了一个指针参数。 接下来我们就要实现这四个接口,我习惯的开发顺序是 数据库操作->业务实现->注册服务。


3.1 数据库操作

新建并编辑task-srv/repository/task.go

package repository

import (
    "context"
    "github.com/pkg/errors"
    pb "go-todolist/task-srv/proto/task"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "log"
    "strings"
    "time"
)

const (
    // 默认数据库名
    DbName = "todolist"
    // 默认表名
    TaskCollection = "task"
    UnFinished     = 0
    Finished       = 1
)

// 定义数据库基本的增删改查操作
type TaskRepository interface {
    InsertOne(ctx context.Context, task *pb.Task) error
    Delete(ctx context.Context, id string) error
    Modify(ctx context.Context, task *pb.Task) error
    Finished(ctx context.Context, task *pb.Task) error
    Count(ctx context.Context, keyword string) (int64, error)
    Search(ctx context.Context, req *pb.SearchRequest) ([]*pb.Task, error)
}

// 数据库操作实现类
type TaskRepositoryImpl struct {
    // 需要注入一个数据库连接客户端
    Conn *mongo.Client
}

// 定义默认的操作表
func (repo *TaskRepositoryImpl) collection() *mongo.Collection {
    return repo.Conn.Database(DbName).Collection(TaskCollection)
}

func (repo *TaskRepositoryImpl) InsertOne(ctx context.Context, task *pb.Task) error {
    _, err := repo.collection().InsertOne(ctx, bson.M{
        "body":       task.Body,
        "startTime":  task.StartTime,
        "endTime":    task.EndTime,
        "isFinished": UnFinished,
        "createTime": time.Now().Unix(),
    })
    return err
}

func (repo *TaskRepositoryImpl) Delete(ctx context.Context, id string) error {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return err
    }
    _, err = repo.collection().DeleteOne(ctx, bson.M{"_id": oid})
    return err
}

func (repo *TaskRepositoryImpl) Modify(ctx context.Context, task *pb.Task) error {
    id, err := primitive.ObjectIDFromHex(task.Id)
    if err != nil {
        return err
    }
    _, err = repo.collection().UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{
        "body":       task.Body,
        "startTime":  task.StartTime,
        "endTime":    task.EndTime,
        "updateTime": time.Now().Unix(),
    }})
    return err
}
func (repo *TaskRepositoryImpl) Finished(ctx context.Context, task *pb.Task) error {
    id, err := primitive.ObjectIDFromHex(task.Id)
    if err != nil {
        return err
    }
    now := time.Now().Unix()
    update := bson.M{
        "isFinished": int32(task.IsFinished),
        "updateTime": now,
    }
    if task.IsFinished == Finished {
        update["finishTime"] = now
    }
    log.Print(task)
    log.Println(update)
    _, err = repo.collection().UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update})
    return err
}

func (repo *TaskRepositoryImpl) Count(ctx context.Context, keyword string) (int64, error) {
    filter := bson.M{}
    if keyword != "" && strings.TrimSpace(keyword) != "" {
        filter = bson.M{"body": bson.M{"$regex": keyword}}
    }
    count, err := repo.collection().CountDocuments(ctx, filter)
    return count, err
}

func (repo *TaskRepositoryImpl) Search(ctx context.Context, req *pb.SearchRequest) ([]*pb.Task, error) {
    filter := bson.M{}
    if req.Keyword != "" && strings.TrimSpace(req.Keyword) != "" {
        filter = bson.M{"body": bson.M{"$regex": req.Keyword}}
    }

    cursor, err := repo.collection().Find(ctx,
        filter,
        options.Find().SetSkip((req.PageCode-1)*req.PageSize),
        options.Find().SetLimit(req.PageSize),
        options.Find().SetSort(bson.M{req.SortBy: req.Order}))
    if err != nil {
        return nil, errors.WithMessage(err, "search mongo")
    }
    var rows []*pb.Task
    if err := cursor.All(ctx, &rows); err != nil {
        return nil, errors.WithMessage(err, "parse data")
    }
    return rows, nil
}

3.2 业务实现

新建并编辑task-srv/handlertask.go这里注意,真正的返回值是通过操作resp参数返回的:

package handler

import (
    "context"
    "github.com/pkg/errors"
    pb "go-todolist/task-srv/proto/task"
    "go-todolist/task-srv/repository"
)

// 要实现接口,首先当然要定义一个结构体
type TaskHandler struct {
    TaskRepository repository.TaskRepository
}

func (t *TaskHandler) Create(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {
    if req.Body == "" || req.StartTime <= 0 || req.EndTime <= 0 {
        return errors.New("bad param")
    }
    if err := t.TaskRepository.InsertOne(ctx, req); err != nil {
        return err
    }
    resp.Msg = "success"
    return nil
}
func (t *TaskHandler) Delete(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {
    if req.Id == "" {
        return errors.New("bad param")
    }
    if err := t.TaskRepository.Delete(ctx, req.Id); err != nil {
        return err
    }
    resp.Msg = req.Id
    return nil
}
func (t *TaskHandler) Modify(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {
    if req.Id == "" || req.Body == "" || req.StartTime <= 0 || req.EndTime <= 0 {
        return errors.New("bad param")
    }
    if err := t.TaskRepository.Modify(ctx, req); err != nil {
        return err
    }
    resp.Msg = "success"
    return nil
}
func (t *TaskHandler) Finished(ctx context.Context, req *pb.Task, resp *pb.EditResponse) error {
    if req.Id == "" || req.IsFinished != repository.UnFinished && req.IsFinished != repository.Finished {
        return errors.New("bad param")
    }
    if err := t.TaskRepository.Finished(ctx, req); err != nil {
        return err
    }
    resp.Msg = "success"
    return nil
}
func (t *TaskHandler) Search(ctx context.Context, req *pb.SearchRequest, resp *pb.SearchResponse) error {
    count, err := t.TaskRepository.Count(ctx, req.Keyword)
    if err != nil {
        return errors.WithMessage(err, "count row number")
    }
    if req.PageCode <= 0 {
        req.PageCode = 1
    }
    if req.PageSize <= 0 {
        req.PageSize = 20
    }
    if req.SortBy == "" {
        req.SortBy = "createTime"
    }
    if req.Order == 0 {
        req.Order = -1
    }
    if req.PageSize*(req.PageCode-1) > count {
        return errors.New("There's not that much data")
    }
    rows, err := t.TaskRepository.Search(ctx, req)
    if err != nil {
        return errors.WithMessage(err, "search data")
    }
    *resp = pb.SearchResponse{
        PageCode: req.PageCode,
        PageSize: req.PageSize,
        SortBy:   req.SortBy,
        Order:    req.Order,
        Rows:     rows,
    }
    return nil
}

3.3 注册服务

最后的最后,我们参考上一章的main.go,注册我们的服务。 这里需要说明的是,在v2版本中go-micro默认使用mdns作为服务发现,他无需安装部署主要用于学习和简单场景开发,生产环境官方建议使用etcd,这个我们在后面的插件部分再套路。 另外,由于本服务暂时没有需要处理的消息,我们删除了消息相关接口。 最后,我们配置了mongo连接作为数据库(关于mongo的搭建请自行搜索):

package main

import (
    "context"
    "github.com/micro/go-micro/v2"
    "github.com/pkg/errors"
    "go-todolist/task-srv/handler"
    pb "go-todolist/task-srv/proto/task"
    "go-todolist/task-srv/repository"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "log"
    "time"
)

// 这里是我内网的mongo地址,请根据你得实际情况配置,推荐使用dockers部署
const MONGO_URI = "mongodb://172.18.0.58:27017"

func main() {
    // 在日志中打印文件路径,便于调试代码
    log.SetFlags(log.Llongfile)

    conn, err := connectMongo(MONGO_URI, time.Second)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Disconnect(context.Background())

    // New Service
    service := micro.NewService(
        micro.Name("go.micro.service.task"),
        micro.Version("latest"),
    )

    // Initialise service
    service.Init()

    // Register Handler
    taskHandler := &handler.TaskHandler{
        TaskRepository: &repository.TaskRepositoryImpl{
            Conn: conn,
        },
    }
    if err := pb.RegisterTaskServiceHandler(service.Server(), taskHandler); err != nil {
        log.Fatal(errors.WithMessage(err, "register server"))
    }

    // Run service
    if err := service.Run(); err != nil {
        log.Fatal(errors.WithMessage(err, "run server"))
    }
}

// 连接到MongoDB
func connectMongo(uri string, timeout time.Duration) (*mongo.Client, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()
    client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
    if err != nil {
        return nil, errors.WithMessage(err, "create mongo connection session")
    }
    return client, nil
}

四、运行并校验

4.1 运行

运行make build或者go build -o task-srv main.go打包程序,这个时候你大概率会看到如下错误:

cab8d8ce461e78bfbf6484f80002b06b.png

这是因为etcd包的一个版本兼容性问题,为什么之前的hello服务没有遇到这个情况呢,查看micro工具自动生成的go.mod文件,会发现如下内容:

// This can be removed once etcd becomes go gettable, version 3.4 and 3.5 is not,
// see https://github.com/etcd-io/etcd/issues/11154 and https://github.com/etcd-io/etcd/issues/11931.
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

根据提示在我们的task-srv/go.mod中加入

replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

再次运行:

> go run main.go
2020-09-10 17:42:46  file=v2@v2.9.1/service.go:200 level=info Starting [service] go.micro.service.task
2020-09-10 17:42:46  file=grpc/grpc.go:864 level=info Server [grpc] Listening on [::]:63776
2020-09-10 17:42:46  file=grpc/grpc.go:697 level=info Registry [mdns] Registering node: go.micro.service.task-816e865d-7d43-4d02-94b4-be11cd475193

终于成功运行。


4.2 task-cli 调用服务

与上一章类似,新建并编辑task-srv/task-cli.go:

package main

import (
    "context"
    "github.com/micro/go-micro/v2"
    "log"
    pb "task-srv/proto/task"
    "task-srv/repository"
    "time"
)

func main() {
    // 在日志中打印文件路径,便于调试代码
    log.SetFlags(log.Llongfile)
    // 客户端也注册为服务
    server := micro.NewService(micro.Name("go.micro.client.task"))
    server.Init()
    taskService := pb.NewTaskService("go.micro.service.task", server.Client())

    // 调用服务生成三条任务
    now := time.Now()
    insertTask(taskService, "完成学习笔记(一)", now.Unix(), now.Add(time.Hour*24).Unix())
    insertTask(taskService, "完成学习笔记(二)", now.Add(time.Hour*24).Unix(), now.Add(time.Hour*48).Unix())
    insertTask(taskService, "完成学习笔记(三)", now.Add(time.Hour*48).Unix(), now.Add(time.Hour*72).Unix())

    // 分页查询任务列表
    page, err := taskService.Search(context.Background(), &pb.SearchRequest{
        PageCode: 1,
        PageSize: 20,
    })
    if err != nil {
        log.Fatal("search1", err)
    }
    log.Println(page)

    // 更新第一条记录为完成
    row := page.Rows[0]
    if _, err = taskService.Finished(context.Background(), &pb.Task{
        Id:         row.Id,
        IsFinished: repository.Finished,
    }); err != nil {
        log.Fatal("finished", row.Id, err)
    }

    // 修改查询到的第二条数据,延长截至日期
    row = page.Rows[1]
    if _, err = taskService.Modify(context.Background(), &pb.Task{
        Id:        row.Id,
        Body:      row.Body,
        StartTime: row.StartTime,
        EndTime:   now.Add(time.Hour * 72).Unix(),
    }); err != nil {
        log.Fatal("modify", row.Id, err)
    }

    // 删除第三条记录
    row = page.Rows[2]
    if _, err = taskService.Delete(context.Background(), &pb.Task{
        Id: row.Id,
    }); err != nil {
        log.Fatal("delete", row.Id, err)
    }

    // 再次分页查询,校验修改结果
    page, err = taskService.Search(context.Background(), &pb.SearchRequest{})
    if err != nil {
        log.Fatal("search2", err)
    }
    log.Println(page)
}
func insertTask(taskService pb.TaskService, body string, start, end int64) {
    _, err := taskService.Create(context.Background(), &pb.Task{
        Body:      body,
        StartTime: start,
        EndTime:   end,
    })
    if err != nil {
        log.Fatal("create", err)
    }
    log.Println("create task success! ")
}

运行后就可以看到如下结果:

>go run task-cli.go
2020-09-11 02:46:17.731409 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:79: create task success!
2020-09-11 02:46:17.732439 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:79: create task success!
2020-09-11 02:46:17.733434 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:79: create task success!
2020-09-11 02:46:17.734431 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:33: pageSize:20  pageCode:1  sortBy:"CreateTime"  order:-1  rows:{id:"5f5a74791f01617808fef8b6"  body:"完成学习笔记(一)
"  startTime:1599763577  endTime:1599849977  createTime:1599763577}  rows:{id:"5f5a74791f01617808fef8b7"  body:"完成学习笔记(二)"  startTime:1599849977  endTime:1599936377  createTime:1599763577}  rows:{id:"5f5a74791f0161
7808fef8b8"  body:"完成学习笔记(三)"  startTime:1599936377  endTime:1600022777  createTime:1599763577}
2020-09-11 02:46:17.738421 I | E:/go-workspace/go-micro-study-notes/go-todolist1/task-srv/task-cli.go:68: pageSize:20  pageCode:1  sortBy:"CreateTime"  order:-1  rows:{id:"5f5a74791f01617808fef8b6"  body:"完成学习笔记(一)
"  startTime:1599763577  endTime:1599849977  isFinished:1  finishTime:1599763577  createTime:1599763577  updateTime:1599763577}  rows:{id:"5f5a74791f01617808fef8b7"  body:"完成学习笔记(二)"  startTime:1599849977  endTime:
1600022777  createTime:1599763577  updateTime:1599763577}

总结

本章我们参照demo示例,从头开始手写了一个完整的微服务task-srv,并且引入了数据库操作。 下一章我们将围绕这个核心服务再开发两个新的微服务,并尝引入之前被我们搁置的消息服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值