go-zero微服务实战系列(四、CRUD热身)

本文涉及的产品
对象存储 OSS,20GB 3个月
日志服务 SLS,月写入数据量 50GB 1个月
对象存储 OSS,内容安全 1000次 1年
简介: go-zero微服务实战系列(四、CRUD热身)

上一篇文章我们把整个项目的架子搭建完成,服务在本地也已经能运行起来了,顺理成章的接下来我们就应该开始写业务逻辑代码了,但是单纯的写业务逻辑代码是比较枯燥的,业务逻辑的代码我会不断地补充到 lerbon 项目中去,关键部分我也会加上注释。

那么本篇文章我主要想和大家分享下服务的基本配置和几个典型的代码示例。

日志定义

go-zero的 logx 包提供了日志功能,默认不需要做任何配置就可以在stdout中输出日志。当我们请求/v1/order/list接口的时候输出日志如下,默认是json格式输出,包括时间戳,http请求的基本信息,接口耗时,以及链路追踪的span和trace信息。

{"@timestamp":"2022-06-11T08:23:36.342+08:00","caller":"handler/loghandler.go:197","content":"[HTTP] 200 - GET /v1/order/list?uid=123 - 127.0.0.1:59998 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","duration":"21.2ms","level":"info","span":"23c4deaa3432fd03","trace":"091ffcb0eafe7818b294e4d8122cf8a1"}

程序启动后,框架会默认输出level为stat的统计日志,用于输出当前资源的使用情况,主要为cpu和内存,内容如下:

{"@timestamp":"2022-06-11T08:34:58.402+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.3Mi, TotalAlloc=7.0Mi, Sys=16.3Mi, NumGC=8","level":"stat"}

当我们不需要这类日志的时候,我们可以通过如下方式关闭该类日志的输出:

logx.DisableStat()

有的时候我们只需要记录错误日志,可以通过设置日志等级来取消level为info级别日志的输出:

logx.SetLevel(logx.ErrorLevel)

可以扩展日志输出的字段,添加了uid字段记录请求的用户的uid,日志打印内容如下:

logx.Infow("order list", logx.Field("uid",req.UID))
{"@timestamp":"2022-06-11T08:53:50.609+08:00","caller":"logic/orderlistlogic.go:31","content":"order list","level":"info","uid":123}

我们还可以扩展其他第三方日志库,通过logx.SetWriter来进行设置

writer := logrusx.NewLogrusWriter(func(logger *logrus.Logger) {
    logger.SetFormatter(&logrus.JSONFormatter{})
})
logx.SetWriter(writer)

同时logx还提供了丰富的配置,可以配置日志输出模式,时间格式,输出路径,是否压缩,日志保存时间等

type LogConf struct {
    ServiceName         string `json:",optional"`
    Mode                string `json:",default=console,options=[console,file,volume]"`
    Encoding            string `json:",default=json,options=[json,plain]"`
    TimeFormat          string `json:",optional"`
    Path                string `json:",default=logs"`
    Level               string `json:",default=info,options=[info,error,severe]"`
    Compress            bool   `json:",optional"`
    KeepDays            int    `json:",optional"`
    StackCooldownMillis int    `json:",default=100"`
}

可以看到logx提供的日志功能还是非常丰富的,同时支持了各种自定义的方式。日志是我们排查线上问题非常重要的依赖,我们还会根据日志做各种告警,所以这里我们先做了一些日志使用的介绍。

服务依赖

在BFF服务中会依赖多个RPC服务,默认情况下,如果依赖的RPC服务没有启动,BFF服务也会启动异常,报错如下,通过日志可以知道是因为order.rpc没有启动,因为order.rpc是整个商城系统的核心服务,BFF对order.rpc是强依赖,在强依赖的情况下如果被依赖服务异常,那么依赖服务也无法正常启动。

{"@timestamp":"2022-06-11T10:21:56.711+08:00","caller":"internal/discovbuilder.go:34","content":"bad resolver state","level":"error"}
2022/06/11 10:21:59 rpc dial: discov://127.0.0.1:2379/order.rpc, error: context deadline exceeded, make sure rpc service "order.rpc" is already started
exit status 1

再看如下的场景,BFF依赖reply.rpc,因为reply.rpc异常导致BFF无法正常启动,由于reply.rpc并不是商城系统的核心依赖,就算reply.rpc挂掉也不影响商城的核心流程,所以对于BFF来说reply.rpc是弱依赖,在弱依赖的情况下不应该影响依赖方的启动。

{"@timestamp":"2022-06-11T11:26:51.711+08:00","caller":"internal/discovbuilder.go:34","content":"bad resolver state","level":"error"}
2022/06/11 11:26:54 rpc dial: discov://127.0.0.1:2379/reply.rpc, error: context deadline exceeded, make sure rpc service "reply.rpc" is already started
exit status 1

在go-zero中提供了弱依赖的配置,配置后BFF即可正常启动,可以看到order.rpc和product.rpc都是强依赖,而reply.rpc配置了NonBlock:true为弱依赖

OrderRPC:
    Etcd:
        Hosts:
          - 127.0.0.1:2379
        Key: order.rpc
ProductRPC:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: product.rpc
ReplyRPC:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: reply.rpc
  NonBlock: true

并行调用

在高并发的系统中,接口耗时是我们非常关注的点,接口快速响应可以提升用户体验,长时间的等待会让用户体验很差,用户也就会慢慢的离开我们。这里我们介绍简单但很实用的提升接口响应时间的方法,那就是并行的依赖调用。

下图展示了串行调用和并行调用的区别,串行调用依赖的话,耗时等于所有依赖耗时的和,并行调用依赖的话,耗时等于所有依赖中耗时最大的一个依赖的耗时。

在获取商品详情的接口中,参数ProductIds为逗号分隔的多个商品id,在这里我们使用go-zero提供的mapreduce来并行的根据商品id获取商品详情,代码如下,详细代码请参考product-rpc服务:

func (l *ProductsLogic) Products(in *product.ProductRequest) (*product.ProductResponse, error) {
    products := make(map[int64]*product.ProductItem)
    pdis := strings.Split(in.ProductIds, ",")
    ps, err := mr.MapReduce(func(source chan<- interface{}) {
        for _, pid := range pdis {
            source <- pid
        }
    }, func(item interface{}, writer mr.Writer, cancel func(error)) {
        pid := item.(int64)
        p, err := l.svcCtx.ProductModel.FindOne(l.ctx, pid)
        if err != nil {
            cancel(err)
            return
        }
        writer.Write(p)
    }, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) {
        var r []*model.Product
        for p := range pipe {
            r = append(r, p.(*model.Product))
        }
        writer.Write(r)
    })
    if err != nil {
        return nil, err
    }
    for _, p := range ps.([]*model.Product) {
        products[p.Id] = &product.ProductItem{
            ProductId: p.Id,
            Name:      p.Name,
        }
    }
    return &product.ProductResponse{Products: products}, nil
}

在商品详情页,不仅展示了商品的详情,同时也展示了商品评价的第一页,然后点击评价详情可以跳转到评价详情页,为了避免客户端同时请求多个接口,所以我们在商品详情页把评论首页的内容一并返回,因为评论内容并不是核心内容所以在这里我们还做了降级,即请求reply.rpc接口报错我们会忽略这个错误,从而能让商品详情正常的展示。因为获取商品详情和商品评价没有前后依赖关系,所以这里我们使用mr.Finish来并行的请求来降低接口的耗时。

func (l *ProductDetailLogic) ProductDetail(req *types.ProductDetailRequest) (resp *types.ProductDetailResponse, err error) {
    var (
        p *product.ProductItem
        cs *reply.CommentsResponse
    )
    if err := mr.Finish(func() error {
        var err error
        if p, err = l.svcCtx.ProductRPC.Product(l.ctx, &product.ProductItemRequest{ProductId: req.ProductID}); err != nil {
            return err
        }
        return nil
    }, func() error {
        var err error
        if cs, err = l.svcCtx.ReplyRPC.Comments(l.ctx, &reply.CommentsRequest{TargetId: req.ProductID}); err != nil {
            logx.Errorf("get comments error: %v", err)
        }
        return nil
    }); err != nil {
        return nil, err
    }
    var comments []*types.Comment
    for _, c := range cs.Comments {
        comments = append(comments, &types.Comment{
            ID: c.Id,
            Content:   c.Content,
        })
    }
    return &types.ProductDetailResponse{
        Product: &types.Product{
            ID:        p.ProductId,
            Name:      p.Name,
        },
        Comments: comments,
    }, nil
}

图片上传

图片上传是非常常用的功能,我们在product-admin中需要上传商品图片,这里我们把商品图片上传到阿里云OSS中,api定义如下

syntax = "v1"
type UploadImageResponse {
    Success bool `json:"success"`
}
service admin-api {
    @handler UploadImageHandler
    post /v1/upload/image() returns (UploadImageResponse)
}

在admin-api.yaml中添加如下配置

Name: admin-api
Host: 0.0.0.0
Port: 8888
OSSEndpoint: https://oss-cn-hangzhou.aliyuncs.com
AccessKeyID: xxxxxxxxxxxxxxxxxxxxxxxx
AccessKeySecret: xxxxxxxxxxxxxxxxxxxxxxxx

添加OSS客户端

type ServiceContext struct {
    Config config.Config
    OssClient *oss.Client
}
func NewServiceContext(c config.Config) *ServiceContext {
    oc, err := oss.New(c.OSSEndpoint, c.AccessKeyID, c.AccessKeySecret)
    if err != nil {
        panic(err)
    }
    return &ServiceContext{
        Config: c,
        OssClient: oc,
    }
}

上传逻辑需要先获取bucket,该bucket为预先定义的bucket,可以通过api调用创建,也可以在阿里云工作台手动创建

func (l *UploadImageLogic) UploadImage() (resp *types.UploadImageResponse, err error) {
    file, header, err := l.r.FormFile(imageFileName)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    bucket, err := l.svcCtx.OssClient.Bucket(bucketName)
    if err != nil {
        return nil, err
    }
    if err = bucket.PutObject(header.Filename, file); err != nil {
        return nil, err
    }
    return &types.UploadImageResponse{Success: true}, nil
}

使用Postman上传图片,注意在上传图片前需要先创建bucket

登录阿里云对象存储查看已上传的图片

结束语

本篇文章通过日志定义和服务依赖介绍了服务构建中常见的一些配置,这里并没有把所有配置一一列举而是举例说明了社区中经常有人问到的场景,后面的文章还会继续不断完善服务的相关配置。接着又通过服务依赖的并行调用和图片上传两个案例展示了常见功能的优化手段以及编码方式。

这里并没有把所有的功能都列出来,也是想起个头,大家可以把项目down下来自己去完善这个项目,纸上得来终觉浅,绝知此事要躬行,当然我也会继续完善项目代码和大家一起学习进步。

希望本篇文章对你有所帮助,谢谢。

每周一、周四更新

代码仓库

项目地址

https://github.com/zeromicro/go-zero

相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
相关文章
|
1月前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
129 53
|
3月前
|
Go API Docker
热门go与微服务15
热门go与微服务15
36 2
|
3月前
|
Shell Go API
Go语言grequests库并发请求的实战案例
Go语言grequests库并发请求的实战案例
|
5天前
|
存储 监控 供应链
微服务拆分的 “坑”:实战复盘与避坑指南
本文回顾了从2~3人初创团队到百人技术团队的成长历程,重点讨论了从传统JSP到前后端分离+SpringCloud微服务架构的演变。通过实际案例,总结了微服务拆分过程中常见的两个问题:服务拆分边界不清晰和拆分粒度过细,并提出了优化方案,将11个微服务优化为6个,提高了系统的可维护性和扩展性。
22 0
|
1月前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
1月前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
1月前
|
运维 NoSQL Java
后端架构演进:微服务架构的优缺点与实战案例分析
【10月更文挑战第28天】本文探讨了微服务架构与单体架构的优缺点,并通过实战案例分析了微服务架构在实际应用中的表现。微服务架构具有高内聚、低耦合、独立部署等优势,但也面临分布式系统的复杂性和较高的运维成本。通过某电商平台的实际案例,展示了微服务架构在提升系统性能和团队协作效率方面的显著效果,同时也指出了其带来的挑战。
85 4
|
2月前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
2月前
|
运维 Go 开发者
Go语言在微服务架构中的应用与优势
本文深入探讨了Go语言在构建微服务架构中的独特优势和实际应用。通过分析Go语言的核心特性,如简洁的语法、高效的并发处理能力以及强大的标准库支持,我们揭示了为何Go成为开发高性能微服务的首选语言。文章还详细介绍了Go语言在微服务架构中的几个关键应用场景,包括服务间通信、容器化部署和自动化运维等,旨在为读者提供实用的技术指导和启发。
|
2月前
|
负载均衡 Go API
探索Go语言在微服务架构中的应用与优势
在这篇技术性文章中,我们将深入探讨Go语言(又称为Golang)在构建微服务架构时的独特优势。文章将通过对比分析Go语言与其他主流编程语言,展示Go在并发处理、性能优化、以及开发效率上的优势。同时,我们将通过一个实际的微服务案例,详细说明如何利用Go语言构建高效、可扩展的微服务系统。