Junedayday Blog

六月天天的个人博客

0%

go-tip

一个简单的main函数

我们先来看看一个最简单的http服务端的实现

1
2
3
4
5
6
7
8
9
10
// http服务
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", hello)
http.ListenAndServe(":8080", mux)
}

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Println("hello")
}

它的功能很简单:提供一个监听在8080端口的服务器,处理URL/hello的请求,并打印出hello。

可以用一个简单的curl请求来打印结果:

1
curl http://localhost:8080/hello

也可以用对应的kill杀死了对应的进程:

1
kill -9 {pid}

但有一个问题:

如果程序因为代码问题而意外退出(例如panic),无法和kill这种人为强制杀死的情况进行区分

阅读全文 »

Go的就业方向

目前,后端开发语言的就业方向主要分为两块:业务系统开发基础平台开发Go语言自然也不会例外。

也许有朋友不太了解这两块,那我简单地解释下:

业务系统开发 主要指公司对外盈利的系统,包括 toBtoC。由于这个是公司安身立命的根本,所以开发者是必须跟着业务走的。

基础平台开发 指的是公司为了提升工作效率(不仅仅是研发),搭建的一套内部体系,常常需要跨业务支持。

目前主流的云平台,其实是包装成一套业务系统的基础平台,比如阿里云的ECS。

这类云平台是大型公司将自己的基础平台能力沉淀下来后,包装成一套业务系统对外销售,内部也是分成了两类开发人员:上层开发一些多语言接口、计费等业务系统;下层开发对应的基础平台。

阅读全文 »

大家好,我是六月天天。从今天开始,我将开启一个新的系列 - Go语言学习路线

大家可以从 bilibili 的视频分享中看到观点的延伸思考

以成长为核心

目前网上已有很多Go相关的教程,包括基础讲解、源码解析、面试技巧等,但我依然下决心来做一个具有强烈个人主观观点色彩的Go语言学习路线:这个系列的目标只有一个 - 成长 。我不求这个系列超越其它的教程,而是能做到去芜存菁 ,引发一批朋友的共鸣。

整个系列中,我会输出大量的主观观点,大家不一定能全盘接受,我也不希望大家全盘接受。我坚信,没有碰撞的技术观点无法引起大家的深度思考 ,犹如上课时老师对你进行单方面的内容输出;同时,主观观点会更具现实色彩与实践意义 ,我经历的公司和方向都很丰富,或多或少会和大家的实际工作产生共鸣。

阅读全文 »

在前面的分析中,我们已经知道了使用proto序列化的代码在encoding目录中,路径中只有三个文件,其中2个还是测试文件,看起来这次的工作量并不大。

首先,针对读源码是先看源代码还是测试代码,因人而异。个人建议在对源码毫无头绪时,先从测试入手,了解大致功能;如果有一定基础,那么也可以直接入手源代码。我认为优秀的Go源码可读性是非常高的,所以一般情况下,我都直接从源文件入手,遇到问题才会去对应的测试里阅读。

Marshal

Marshal的代码不多,关键在于传入参数的类型,有2个分支路线:

  1. proto.Marshaler类型,实现了Marshal() ([]byte, error)方法
  2. proto.Message类型,实现了Reset()String() stringProtoMessage()三个方法

我们回头看看proto生成的go文件,发现对应的是第二个接口。那我们接着看:

  1. 调用了protoBufferPool,是一个sync.Pool,是为了加速proto对象的分配
  2. 内部采用的是 marshalAppend,字面来看就是 序列化并追加,对应了 wire-format这个概念,并不需要将整个结构加载完毕、再进行序列化
  3. 接下来调用的是protoV2.MarshalOptions,需要关注的是protoV2是另一个package,protoV2 "google.golang.org/protobuf/proto"
  4. 在正式marshal前,调用m.ProtoReflect()方法,根据名字可以猜测是对Message做反射,详细内容不妨后面再看
  5. 最后就是正式的marshal了,分两个分支:out, err = methods.Marshal(in)out.Buf, err = o.marshalMessageSlow(b, m)。后者是慢速的,一般情况下是不会用到,我们重点关注前者,这时就需要回头看4中的实现了
  6. 逐个往前搜索,接口protoreflect.Message => 接口Message =>函数MessageV2 => 函数ProtoMessageV2Of => 函数legacyWrapMessage => 函数MessageOf => 类型messageReflectWrapper,终于,在这里找到了目标函数 ProtoMethods
  7. 因为我们取的是methods,所以很快将代码定位到 makeCoderMethods => marshal => marshalAppendPointer ,最后找到一行核心代码 b, err = f.funcs.marshal(b, fptr, f, opts)
  8. 那这个marshal什么时候被赋值的呢?在步骤7中,我们查看了methods被赋值的地方,其实旁边就有一个函数 makeReflectFuncs ,最后定位到了 /google.golang.org/protobuf/internal/impl/codec_gen.go 文件中。每种变量的序列化,都是按照特定规则来执行的。

实战

那么 protobuf 实际是如何对每种类型进行Encoding的呢?有兴趣的朋友可以点击这个链接,阅读原文。这里,我直接拿出一个实例进行讲解。

定义proto

1
2
3
4
5
message People {
bool male = 1;
int32 age = 2;
string address = 3;
}

生成对应文件后,编写测试用例

1
2
3
4
5
6
7
8
9
func main() {
people := &pbmsg.People{
Male: true,
Age: 80,
Address: "China Town",
}
b, _ := proto.Marshal(people)
fmt.Printf("%b\n", b)
}

运行生成结果

1
[1000 1 10000 1010000 11010 1010 1000011 1101000 1101001 1101110 1100001 100000 1010100 1101111 1110111 1101110]

分析第一个字段Bool

首先,Male是一个bool字段,序号为1。

根据Google上的文档,bool是Varint,所以计算

(field_number << 3) | wire_type = (1<<3)|0 = 8,对应第一个字节: 1000

然后,它的值true对应第二个字节1

分析第二个字段Int

同样的,(field_number << 3) | wire_type = (2<<3)|0 = 16,对应第三个字节10000

值80对应1010000

分析第三个字段String

因为string是不定长的,所以需要一个额外的长度字段

(field_number << 3) | wire_type = (3<<3)|2=26,对应11010

接下来是长度字段,我们有10个英文单词,所以长度为10,对应 1010

然后就是10个Byte表示”China Town”了

结语

本次的分析到这里就暂时告一段落了,阅读protobuf的相关代码还是非常耗时耗力的。其实这块最主要的复杂度在于为了兼容新老版本,采用了大量的Interface实现。Interface带有面向对象特色,在重构代码时很有意义,不过也给阅读代码时,查找方法对应实现时带来了复杂度。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding

官方Git总览

我们先看看GRPC这个项目的总览,主要分三种:

  • 基于C实现,包括了 C++, Python, Ruby, Objective-C, PHP, C#
  • 其余语言实现的,最主要是go,java,node
  • proposal,即grpc的RFC,关于实现、讨论的文档汇总

从这里可以看出,gRPC虽然是支持多语言,但原生的实现并不多。如果想在一些小众语言里引入gRPC,还是有很大风险的,有兴趣的可以搜索下TiDB在探索rust的gRPC的经验分享。

gRPC-Go

作为一名Go语言开发者,我自然选择从最熟悉的语言入手。同时,值得注意的是,grpc-go是除了C家族系列以外使用量最大的repo,加上Go语言优秀的可读性,是一个很好的入门gRPC的阅读材料。

进入项目,整个README.md文档也不长。通常情况下,如果你能啃完这个文档及相关链接,你对这个开源项目就已经超过99%的人了。

对Repo的相关注意事项,大家逐行阅读即可,整体比较简单,我简单列举下关键点:

  1. 建议阅读官网文档(恭喜你,上次我们已经读完了官方文档)
  2. 在项目中的引入,建议用go mod
  3. 优先支持3个Go语言最新发布的版本
  4. FAQ中的常见问题,主要关注package下载问题如何开启追踪日志

通读完成,我们再深入看看文档细节,Example这块我们在官网的测试中已经看过,我们的接下来重点是godoc和具体细节的文档。

go doc

DefaultBackoffConfig

注意,这个变量被弃用,被挪到 ConnectParams里了(详情链接)。那这个所谓的连接参数是什么用呢?代码不长,我们选择几个比较重要的内容来阅读下,原链接可以点击这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Backoff returns the amount of time to wait before the next retry given the
// number of retries.
// 根据retries返回等待时间,可以认为是一种退避策略
func (bc Exponential) Backoff(retries int) time.Duration {
if retries == 0 {
// 之前没有retries过,就返回BaseDelay
return bc.Config.BaseDelay
}
backoff, max := float64(bc.Config.BaseDelay), float64(bc.Config.MaxDelay)
// 等待时间不能超过max,等待时间 = BaseDelay * Multiplier的retries次方
// Multiplier默认1.6,并不是官方http包中的2
for backoff < max && retries > 0 {
backoff *= bc.Config.Multiplier
retries--
}
if backoff > max {
backoff = max
}
// Randomize backoff delays so that if a cluster of requests start at
// the same time, they won't operate in lockstep.
// 乘以一个随机因子,数值为(1-Jitter,1+Jitter),默认为(0.8,1.2),防止同一时刻有大量请求发出,引起锁的问题
backoff *= 1 + bc.Config.Jitter*(grpcrand.Float64()*2-1)
if backoff < 0 {
return 0
}
return time.Duration(backoff)
}

EnableTracing

用来设置是否开启 trace,追踪日志

Code

gRPC的错误码,原代码见链接,我们大概了解其原因即可:

  • OK 正常
  • Canceled 客户端取消
  • Unknown 未知
  • InvalidArgument 未知参数
  • DeadlineExceeded 超时
  • NotFound 未找到资源
  • AlreadyExists 资源已经创建
  • PermissionDenied 权限不足
  • ResourceExhausted 资源耗尽
  • FailedPrecondition 前置条件不满足
  • Aborted 异常退出
  • OutOfRange 超出范围
  • Unimplemented 未实现方法
  • Internal 内部问题
  • Unavailable 不可用状态
  • DataLoss 数据丢失
  • Unauthenticated 未认证

读完上面的内容,发现跟HTTP/1.1的Status Code非常相似。

CallOption

调用在客户端 Invoke 方法中,包括before发送前,after为接收后。

官方提供了几个常用的CallOption,按场景调用。

ClientConn

抽象的客户端连接。

值得注意的是,conns是一个map,所以实际可能有多个tcp连接。

CodeC

定义了Marshal和Unmarshal的接口,在grpc底层实现是proto,详细可见 codec

Compressor

压缩相关的定义

MetaData

元数据,也就是key-value,可以类比到http的header

DialOption

客户端新建连接时的选项,按场景调用。

ServerOption

服务端监听时的选项,按场景调用。

文档

文档链接

benchmark

性能测试,有兴趣的可以细看gRPC是从哪几个维度做RPC性能测试的。

Compression

可用encoding.RegisterCompressor实现自定义的压缩方法。

注意,压缩算法应用于客户端和服务端两侧。

Concurrency

支持并发,从三个角度分析:

  • ClientConn支持多个Goroutine
  • Steams中,SendMsg/RecvMsg可分别在两个Goroutine中运行,但任何一个方法运行在多个Goroutine上是不安全的
  • Server每个客户端的invoke会对应一个Server端的Goroutine

Encoding

类似Compression,可用encoding.RegisterCodec实现自定义的序列化方法。

go mock

用mock生成测试代码,详细可细看。

Authentication

认证的相关选项,包括 TLS/OAuth2/GCE/JWT ,一般用前两者即可。

Metadata

介绍了Metadata的使用,类比于HTTP/1.1的Header。

Keepalive

长连接的参数分为3类:

  • ClientParameters 客户端侧参数,主要用来探活
  • SeverParameters 服务端参数,控制连接时间
  • EnforcementPolicy 服务端加强型参数

log level

四个级别的log level,针对不同场景:

  • Info 用于debug问题
  • Warning 排查非关键性的问题
  • Error gRPC调用出现无法返回到客户端的问题
  • Fatal 导致程序无法恢复的致命问题

proxy

使用默认的HTTP或HTTPS代理。

rpc error

结合官方提供的错误码,用 status.New 或者 status.Error 创建错误。

server reflection

服务端方法映射,跟着教程走即可。

值得一提的是,采用c++中的grpc_cli模块,可以查看指定端口暴露出来的服务详情。

versioning

版本演进,一般情况下每6周一个小版本,紧急修复会打补丁号。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding

在第一部分,我们学习了gRPC的基本调用过程,这样我们对全局层面有了一定了解。接下来,我们将结合官方文档,继续深入学习、探索下去。

1. Authentication 认证的实现

官方示例

示例很简单,客户端和服务端都大致分为两步:

  1. 生成对应的认证信息 creds
  2. 将认证信息作为 DialOption 传入信息

认证方法的底层实现并不在我们今天的讨论范围内。这里值得一提的是,由于请求会存在大量的输入参数,这里提供的方法是 opts ...DialOption,也就是可变长度的参数,这一点很值得我们思考和学习。

客户端的认证实现

第一步:将认证信息放入连接中

  • grpc.WithTransportCredentials 中,将creds 保存到copts.TransportCredentials
  • 调用Dial,在内部用 opt.apply(&cc.dopts)将认证信息传递到结构中
  • credsClone = creds.Clone() 使用了一份复制,放到了Balancer中,估计是用于负载均衡的,暂时不用考虑

第二步:将认证信息请求中发出

  • 首先我们先找到 Invoke函数,这里是发送请求的入口(对这一块有疑问的,查看上一篇)
  • 分析一下函数 invoke ,调用了newClientStream,一大段代码都没有用到copts.TransportCredentials中的参数,大致猜测是在clientStream
  • 接下来这块,只通过阅读代码,要找到对应使用到copts.TransportCredentials很麻烦,建议第一次可以先通过反向查找,调用到这个参数的地方
  • newHTTP2Client => NewClientTransport => createTransport => tryAllAddrs => resetTransport => connect => getReadyTransport =>pick => getTransport =>newAttemptLocked => newAttemptLocked => newClientStream
  • 这时,我们再正向梳理一下其调用逻辑,大致是查找连接情况,对传输层进行初始化。如果你了解认证是基于传输层Transport的,那下次正向查找时,会有一条比较明确的方向了

服务端的认证实现

第一步:将认证信息放入Server结构中

  • creds包装成ServerOption,传入NewServer
  • 类似Client中的操作,被存至 opts.creds

第二步:在连接中进行认证

  • 参考之前一讲的分析,我们进入函数 handleRawConn
  • 这次,我们的进展很顺利,一下子就看到了关键函数名useTransportAuthenticator
  • 在这里,调用了creds实现的ServerHandshake实现了认证。到这里,认证已经完成,不过我们可以再看看,认证信息是怎么传递的
  • 接着,认证信息传入了 newHTTP2Transport,保存到结构体http2Server中的authInfo,最后返回了一个Interface ServerTransport
  • 在进行连接时,调用了serveStreams,然后调用了 http2ServerHandleStreams方法,这时,我们大致可以猜测,auth在这里被用到了
  • 往下看,发现有个对header帧的处理operateHeaders,在这里被赋值到 pr.AuthInfo里,并被保存到s的Context中
  • 一般情况下,Context的调用是十分隐蔽的,我们可以通过反向查找,哪里调用了peer.FromContext,然而并没有地方应用,那认证的分析,就告一段落了

2. 四类gRPC调用的实现

这一块我们暂不深入源码,先了解使用时的特性

2.1 简单RPC

代码链接

代码逻辑很直观,即处理后返回

2.2 服务端流式RPC

代码链接

代码的关键在于两个函数inRangestream.Send

2.3 客户端流式RPC

代码链接

用一个for循环进行多次发送,stream.Recv()实现了从服务端获取数据,当EOF时,才调用stream.SendAndClose结束发送

2.4 双向流式RPC

代码链接

SendAndClose 变为 Send,其余基本不变。从这里可以看到,正常的关闭都是由服务端发起的。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding

准备工作

参考官方文档,进行部署并运行成功

分析思路:GRPC是怎么实现方法调用的

  1. 分析PB生成的对应文件
  2. 运行server
  3. 运行client

1. 分析PB生成的对应文件

HelloRequest/HelloReply 结构分析

存在三个冗余字段 XXX_NoUnkeyedLiteral XXX_unrecognized XXX_sizecache

这部分主要是兼容proto2的,我们暂时不用细究

GreeterClient客户端

传入一个 cc grpc.ClientConnInterface 客户端连接

可调用的方法为SayHello,其内部的method为”/helloworld.Greeter/SayHello”,也就是/{package}.{service}/{method} ,作为一个唯一的URI

GreeterServer服务端

需要自己实现一个SayHello的方法

其中有个 UnimplementedGreeterServer 的接口,可以嵌入到对应的server结构体中(有方法未实现时,会返回codes.Unimplemented)

2. 运行server

定义server

这里pb.UnimplementedGreeterServer被嵌入了server结构,所以即使没有实现SayHello方法,编译也能通过。

但是,我们通常要强制server在编译期就必须实现对应的方法,所以生产中建议不嵌入。

实现自己的业务逻辑

1
2
3
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error){
//
}

注册TCP监听端口

1
lis, err := net.Listen("tcp", port)

因为gRPC的应用层是基于HTTP2的,所以这里不出意外,监听的是tcp端口

grpc.NewServer()

  1. 入参为选项参数options
  2. 自带一组defaultServerOptions,最大发送size、最大接收size、连接超时、发送缓冲、接收缓冲
  3. s.cv = sync.NewCond(&s.mu) 条件锁,用于关闭连接
  4. 全局参数 EnableTraciing ,会调用golang.org/x/net/trace 这个包

pb.RegisterGreeterServer(s, &server{})

对比自己创建的server和pb中定义的server,确定每个方法都已经实现

service放在 m map[string]*service 中,所以一个server可以放多个proto定义的服务

内部的method和stream放在 service 中的两个map中

s.Serve(lis)

  1. listener 放到内部的map中
  2. for循环,进行tcp连接,这一部分和http源码中的ListenAndServe极其类似
  3. 在协程中进行handleRawConn
  4. 将tcp连接封装对应的creds认证信息
  5. 新建newHTTP2Transport传输层连接
  6. 在协程中进行serveStreams,而http1这里为阻塞的
  7. 函数HandleStreams中参数为2个函数,前者为处理请求,后者用于trace
  8. 进入handleStream,前半段被拆为service,后者为method,通过map查找
  9. method在processUnaryRPC处理,stream在processStreamingRPC处理,这两块内部就比较复杂了,涉及到具体的算法,以后有时间细读

3. 运行client

grpc.Dial

新建一个conn连接,这里是一个支持HTTP2.0的客户端,暂不细讲

pb.NewGreeterClient(conn)

新建一个client,包装对应的method,方便调用SayHello

调用SayHello

1
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
  1. 核心调用的是 Invoke 方法,具体实现要看grpc.ClientConn中
  2. grpc.ClientConn中实现了Invoke方法,在call.go文件中,详情都在invoke中

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding

注:本文的灵感来源于GOPHER 2020年大会陈皓的分享,原PPT的链接可能并不方便获取,所以我下载了一份PDF到git仓,方便大家阅读。我将结合自己的实际项目经历,与大家一起细品这份文档。

目录

今天,我会抛开官方的定义,简单介绍一下三种设计模式。

后续会有介绍Go语言设计模式Design Patterns的系列,会更具理论性。

Decoration

代码实例

1
2
3
4
5
6
7
func decorator(f func(s string)) func(s string) {
return func(s string) {
fmt.Println("Started")
f(s)
fmt.Println("Done")
}
}

一句话解释:在函数f前后,添加装饰性的功能函数,但不改变函数本身的行为

这种设计模式,对一些被高频率调用的代码非常有用:

  1. HTTP Server被调用的handler
  2. HTTP Client发送请求
  3. 对MySQL的操作

而装饰性的功能,常见的有:

  1. 打印相关的日志信息(Debug中非常有用!)
  2. 耗时相关的计算
  3. 监控埋点

Pipeline

代码示例

1
2
3
4
5
6
7
8
type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc
func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
for i := range decors {
d := decors[len(decors)-1-i] // iterate in reverse
h = d(h)
}
return h
}

一句话解释:用不定参数的特性,将入参中的函数,逐个应用到对象上

看到这里,如果你能想起之前 Functional Option 那篇,会发现有这块的影子。

主要应用于: 有多种可选择的配置(对应Field)或处理(对应方法)的复杂对象。

耗子叔在后面又增加了一些用Goroutine+Channel的方式,其实就是讲Channel作为一个管道的承载体。

Visitor

关于访问者设计者模式,我之前在Kubernetes源码分析中专门分析了源码。今天,我们也简单地过一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义访问的函数类型
type VisitorFunc func(*Info, error) error

// Visitor接口设计
type Visitor interface {
Visit(VisitorFunc) error
}

// 资源对象
type Info struct {
Namespace string
Name string
OtherThings string
}

// 将Visitor函数应用到资源对象上
func (info *Info) Visit(fn VisitorFunc) error {
return fn(info, nil)
}

然后看其中一个实现:NameVisitor,其余的也类似,这样就能注入对应的Visitor

1
2
3
4
5
6
7
8
9
10
11
12
type NameVisitor struct {
visitor Visitor
}

func (v NameVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
// 这里是运行入参中的VisitorFunc,这一块的逻辑有点像pipeline
err = fn(info, err)
// NameVisitor自己实现的Visit逻辑
return err
})
}

当然,Kubernetes中的Visitor还有进一步的封装,包括遇到错误时的处理,这里不细讲,有兴趣的朋友可以看看我对那一篇的分析。

Visitor模式最大的优点就是 解耦了数据和程序。回头看Kubernetes的Visitor应用场景,主要是从各种输入源中解析出资源Info。这个过程中Info是数据,各类解析方法是资源。

所以,我认为Visitor模式比较适合的是:目标数据明确,但获取数据的方法多样且复杂。但由于多层Visitor调用复杂,建议大家可以在外面再简单地封一层,提供常用的几种Visitor组合后的接口,供使用方调用。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding

注:本文的灵感来源于GOPHER 2020年大会陈皓的分享,原PPT的链接可能并不方便获取,所以我下载了一份PDF到git仓,方便大家阅读。我将结合自己的实际项目经历,与大家一起细品这份文档。

目录

Simple Script

为了让大家快速了解这块,我们从一个最简单的例子入手。

Template

首先创建一个模板Go文件,即容器模板:container.tmp.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package PACKAGE_NAME
type GENERIC_NAMEContainer struct {
s []GENERIC_TYPE
}
func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer {
return &GENERIC_NAMEContainer{s: []GENERIC_TYPE{}}
}
func (c *GENERIC_NAMEContainer) Put(val GENERIC_TYPE) {
c.s = append(c.s, val)
}
func (c *GENERIC_NAMEContainer) Get() GENERIC_TYPE {
r := c.s[0]
c.s = c.s[1:]
return r
}

Shell

生成的shell脚本,gen.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

set -e

SRC_FILE=${1}
PACKAGE=${2}
TYPE=${3}
DES=${4}
#uppcase the first char
PREFIX="$(tr '[:lower:]' '[:upper:]' <<< ${TYPE:0:1})${TYPE:1}"

DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go

sed 's/PACKAGE_NAME/'"${PACKAGE}"'/g' ${SRC_FILE} | \
sed 's/GENERIC_TYPE/'"${TYPE}"'/g' | \
sed 's/GENERIC_NAME/'"${PREFIX}"'/g' > ${DES_FILE}

四个参数分别为

  • 源文件名
  • 包名
  • 类型
  • 文件后缀名

Generate File

最后,增加一个创建代码的go文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//go:generate ./gen.sh ./template/container.tmp.go gen uint32 container
func generateUint32Example() {
var u uint32 = 42
c := NewUint32Container()
c.Put(u)
v := c.Get()
fmt.Printf("generateExample: %d (%T)\n", v, v)
}

//go:generate ./gen.sh ./template/container.tmp.go gen string container
func generateStringExample() {
var s string = "Hello"
c := NewStringContainer()
c.Put(s)
v := c.Get()
fmt.Printf("generateExample: %s (%T)\n", v, v)
}

Generation

我们运行一下 go generate,就能产生对应的文件。

  1. 运行go generate,工具会扫描所有的文件
  2. 如果发现注释有带 go:generate的,会自动运行后面的命令
  3. 通过命令生成的代码,会在源文件添加提示,告诉他人这是自动生成的代码,不要编辑

因此,我们不仅仅可以用shell脚本,也可以用各种二进制工具来生成代码。值得一提的是,像Kubernetes这种重量级的项目,大量地应用了这种特性。后面我也会和大家分享在开发web项目中的应用。

下面,我也来介绍几个个人认为比较有用的工具。

genny

源项目链接:https://github.com/cheekybits/genny

Go文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package queue

import "github.com/cheekybits/genny/generic"

// NOTE: this is how easy it is to define a generic type
type Something generic.Type

// SomethingQueue is a queue of Somethings.
type SomethingQueue struct {
items []Something
}

func NewSomethingQueue() *SomethingQueue {
return &SomethingQueue{items: make([]Something, 0)}
}
func (q *SomethingQueue) Push(item Something) {
q.items = append(q.items, item)
}
func (q *SomethingQueue) Pop() Something {
item := q.items[0]
q.items = q.items[1:]
return item
}

脚本

1
cat source.go | genny gen "Something=string"

官方示例还是采用的是shell脚本,建议替换到 go:generate 中,这样的代码更统一

原理

可以简单地理解成一个类型替换的工具(PS:擅长用sed脚本的朋友也可直接通过shell脚本实现)

go-bindata

源网站链接:https://github.com/go-bindata/go-bindata

go-bindata的功能是将任意格式的源文件,转化为Go代码,使我们无需再去打开文件读取了。

这个工具多用在静态网页转化为Go代码(不符合前后端分离的实践),所以具体的使用方式我就不细讲了,大家有兴趣的可以自行阅读教程。

但它有两个优点值得我们关注:无需再进行文件读取操作、压缩。

stringer

stringer是官方提供一个字符串工具,我个人非常推荐大家使用

文档链接:https://pkg.go.dev/golang.org/x/tools/cmd/stringer

Go文件

1
2
3
4
5
6
7
8
9
10
11
package painkiller

type Pill int

const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)

脚本

1
//go:generate stringer -type=Pill

于是,就会生成对应的方法func (Pill) String() string,也就是直接转化成了其命名。

价值

Go语言在调用 fmt 等相关包时,如果要将某个变量转化为字符串,默认会寻找它的String()方法。

这时,良好的命名 能体现出其价值。尤其是在错误码的处理上,无需再去查询错误码对应的错误内容,直接可以通过命名了解。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding

注:本文的灵感来源于GOPHER 2020年大会陈皓的分享,原PPT的链接可能并不方便获取,所以我下载了一份PDF到git仓,方便大家阅读。我将结合自己的实际项目经历,与大家一起细品这份文档。

目录

Map/Reduce/Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func MapUpCase(arr []string, fn func(s string) string) []string {
var newArray = []string{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}

func MapLen(arr []string, fn func(s string) int) []int {
var newArray = []int{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}

func Reduce(arr []string, fn func(s string) int) int {
sum := 0
for _, it := range arr {
sum += fn(it)
}
return sum
}

func Filter(arr []string, fn func(n string) bool) []string {
var newArray = []string{}
for _, it := range arr {
if fn(it) {
newArray = append(newArray, it)
}
}
return newArray
}

func main() {
var list = []string{"Hao", "Chen", "MegaEase"}

// 元素一对一映射 string->string
x := MapUpCase(list, func(s string) string {
return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
// [HAO CHEN MEGAEASE]

// 元素一对一映射 string->int
y := MapLen(list, func(s string) int {
return len(s)
})
fmt.Printf("%v\n", y)
// [3 4 8]

// 归约:多个元素->一个元素
z := Reduce(list, func(s string) int {
return len(s)
})
fmt.Printf("%v\n", z)
// 15

// 过滤:过滤不满足条件的元素
f := Filter(list, func(s string) bool {
return len(s) > 3
})
fmt.Printf("%v\n", f)
// [Chen MegaEase]
}

Scenarios

  • Map 是一对一的场景,是 循环中对数据加工处理
  • Reduce 是多对一,是 数据聚合处理
  • Filter是过滤的处理,是 数据有效性

我们以常见的账单统计相关的功能,我们会遇上大量的此类情况:

  1. 统计消费总额 - Reduce
  2. 统计用户A - Filter
  3. 统计本月 - Filter
  4. 费用转化为美金 - Map

在综合各个因素后,就是大量复杂的、管道式的Map/Reduce/Filter操作。

延伸思考一下,这块和SQL语句非常类似

Generic

耗子叔在接下来的部分,展示了用reflect处理泛型情况。我这边简单地截取Map部分解析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func TransformInPlace(slice, function interface{}) interface{} {
return transform(slice, function, true)
}

// map的转换函数,slice为切片,function为对应的函数,inPlace表示是否原地处理
func transform(slice, function interface{}, inPlace bool) interface{} {
// 类型判断,必须为切片
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic("transform: not slice")
}

// 函数的签名判断,即函数的入参必须和slice里的元素一致
fn := reflect.ValueOf(function)
elemType := sliceInType.Type().Elem()
if !verifyFuncSignature(fn, elemType, nil) {
panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType")
}

// 如果是原地,则直接处理函数,结果会保存到入参中(这时入参一般为指针)
// 如果非原地,那就需要新建一个切片,用来保存结果
sliceOutType := sliceInType
if !inPlace {
sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())
}
for i := 0; i < sliceInType.Len(); i++ {
sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])
}
return sliceOutType.Interface()

}

func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {
// 类型判断
if fn.Kind() != reflect.Func {
return false
}

// 入参数量和函数签名一致,出参必须只有一个
if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
return false
}

// 每个函数入参的类型校验
for i := 0; i < len(types)-1; i++ {
if fn.Type().In(i) != types[i] {
return false
}
}

// 出参类型的校验
outType := types[len(types)-1]
if outType != nil && fn.Type().Out(0) != outType {
return false
}
return true
}

仔细阅读这一块代码,我们能学到很多反射方面的知识,尤其是并不常用的函数相关的。

但是,我不建议大家在实际项目中直接使用这一块代码,毕竟其中大量的反射操作是比较耗时的,尤其是在延迟非常敏感的web服务器中。

如果我们多花点时间、直接编写指定类型的代码,那么就能在编译期发现错误,运行时也可以跳过反射的耗时。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding