为何我们需要持续学习
终身成长 一词已被广泛认可,意味着我们将比前人花费更多的时间在 学习成长 中,才能将个人的认知跟上社会的步伐。且不论是否应该放慢脚步,但我们大部分人不得不跟随社会的节奏,持续学习并提高自己。
相信有不少朋友跟我一样,在学习的过程中经常会 半途而废。那么我在这里分享一些方法和技巧(包括但不仅限于Go语言),希望能给大家带来一些启发。
这部分内容依旧会带有一些强烈的个人主观色彩,大家按需选用~
我们先来看看一个最简单的http服务端
的实现
1 | // http服务 |
它的功能很简单:提供一个监听在8080
端口的服务器,处理URL
为/hello
的请求,并打印出hello。
可以用一个简单的curl请求来打印结果:
1 | curl http://localhost:8080/hello |
也可以用对应的kill
杀死了对应的进程:
1 | kill -9 {pid} |
但有一个问题:
如果程序因为代码问题而意外退出(例如panic),无法和kill这种人为强制杀死的情况进行区分
目前,后端开发语言的就业方向主要分为两块:业务系统开发 与 基础平台开发 。Go
语言自然也不会例外。
也许有朋友不太了解这两块,那我简单地解释下:
业务系统开发 主要指公司对外盈利的系统,包括 toB
与 toC
。由于这个是公司安身立命的根本,所以开发者是必须跟着业务走的。
基础平台开发 指的是公司为了提升工作效率(不仅仅是研发),搭建的一套内部体系,常常需要跨业务支持。
目前主流的云平台,其实是包装成一套业务系统的基础平台,比如阿里云的ECS。
这类云平台是大型公司将自己的基础平台能力沉淀下来后,包装成一套业务系统对外销售,内部也是分成了两类开发人员:上层开发一些多语言接口、计费等业务系统;下层开发对应的基础平台。
大家好,我是六月天天。从今天开始,我将开启一个新的系列 - Go语言学习路线。
大家可以从 bilibili 的视频分享中看到观点的延伸思考
目前网上已有很多Go相关的教程,包括基础讲解、源码解析、面试技巧等,但我依然下决心来做一个具有强烈个人主观观点色彩的Go语言学习路线:这个系列的目标只有一个 - 成长 。我不求这个系列超越其它的教程,而是能做到去芜存菁 ,引发一批朋友的共鸣。
整个系列中,我会输出大量的主观观点,大家不一定能全盘接受,我也不希望大家全盘接受。我坚信,没有碰撞的技术观点无法引起大家的深度思考 ,犹如上课时老师对你进行单方面的内容输出;同时,主观观点会更具现实色彩与实践意义 ,我经历的公司和方向都很丰富,或多或少会和大家的实际工作产生共鸣。
在前面的分析中,我们已经知道了使用proto序列化的代码在encoding目录中,路径中只有三个文件,其中2个还是测试文件,看起来这次的工作量并不大。
首先,针对读源码是先看源代码还是测试代码,因人而异。个人建议在对源码毫无头绪时,先从测试入手,了解大致功能;如果有一定基础,那么也可以直接入手源代码。我认为优秀的Go源码可读性是非常高的,所以一般情况下,我都直接从源文件入手,遇到问题才会去对应的测试里阅读。
Marshal的代码不多,关键在于传入参数的类型,有2个分支路线:
Marshal() ([]byte, error)
方法Reset()
、String() string
和ProtoMessage()
三个方法我们回头看看proto生成的go文件,发现对应的是第二个接口。那我们接着看:
marshalAppend
,字面来看就是 序列化并追加,对应了 wire-format
这个概念,并不需要将整个结构加载完毕、再进行序列化protoV2.MarshalOptions
,需要关注的是protoV2是另一个package,protoV2 "google.golang.org/protobuf/proto"
m.ProtoReflect()
方法,根据名字可以猜测是对Message做反射,详细内容不妨后面再看out, err = methods.Marshal(in)
和out.Buf, err = o.marshalMessageSlow(b, m)
。后者是慢速的,一般情况下是不会用到,我们重点关注前者,这时就需要回头看4中的实现了接口protoreflect.Message =>
接口Message
=>函数MessageV2
=> 函数ProtoMessageV2Of
=> 函数legacyWrapMessage
=> 函数MessageOf
=> 类型messageReflectWrapper
,终于,在这里找到了目标函数 ProtoMethods
methods
,所以很快将代码定位到 makeCoderMethods
=> marshal
=> marshalAppendPointer
,最后找到一行核心代码 b, err = f.funcs.marshal(b, fptr, f, opts)
makeReflectFuncs
,最后定位到了 /google.golang.org/protobuf/internal/impl/codec_gen.go
文件中。每种变量的序列化,都是按照特定规则来执行的。那么 protobuf 实际是如何对每种类型进行Encoding的呢?有兴趣的朋友可以点击这个链接,阅读原文。这里,我直接拿出一个实例进行讲解。
1 | message People { |
1 | func main() { |
1 | [1000 1 10000 1010000 11010 1010 1000011 1101000 1101001 1101110 1100001 100000 1010100 1101111 1110111 1101110] |
首先,Male是一个bool字段,序号为1。
根据Google上的文档,bool是Varint,所以计算
(field_number << 3) | wire_type = (1<<3)|0 = 8,对应第一个字节: 1000
然后,它的值true对应第二个字节1
同样的,(field_number << 3) | wire_type = (2<<3)|0 = 16,对应第三个字节10000
值80对应1010000
因为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
我们先看看GRPC这个项目的总览,主要分三种:
从这里可以看出,gRPC虽然是支持多语言,但原生的实现并不多。如果想在一些小众语言里引入gRPC,还是有很大风险的,有兴趣的可以搜索下TiDB在探索rust的gRPC的经验分享。
作为一名Go语言开发者,我自然选择从最熟悉的语言入手。同时,值得注意的是,grpc-go是除了C家族系列
以外使用量最大的repo,加上Go语言优秀的可读性,是一个很好的入门gRPC的阅读材料。
进入项目,整个README.md文档也不长。通常情况下,如果你能啃完这个文档及相关链接,你对这个开源项目就已经超过99%的人了。
对Repo的相关注意事项,大家逐行阅读即可,整体比较简单,我简单列举下关键点:
通读完成,我们再深入看看文档细节,Example这块我们在官网的测试中已经看过,我们的接下来重点是godoc和具体细节的文档。
注意,这个变量被弃用,被挪到 ConnectParams
里了(详情链接)。那这个所谓的连接参数是什么用呢?代码不长,我们选择几个比较重要的内容来阅读下,原链接可以点击这里。
1 | // Backoff returns the amount of time to wait before the next retry given the |
用来设置是否开启 trace,追踪日志
gRPC的错误码,原代码见链接,我们大概了解其原因即可:
读完上面的内容,发现跟HTTP/1.1的Status Code非常相似。
调用在客户端 Invoke
方法中,包括before发送前,after为接收后。
官方提供了几个常用的CallOption,按场景调用。
抽象的客户端连接。
值得注意的是,conns是一个map,所以实际可能有多个tcp连接。
定义了Marshal和Unmarshal的接口,在grpc底层实现是proto,详细可见 codec
压缩相关的定义
元数据,也就是key-value,可以类比到http的header
客户端新建连接时的选项,按场景调用。
服务端监听时的选项,按场景调用。
性能测试,有兴趣的可以细看gRPC是从哪几个维度做RPC性能测试的。
可用encoding.RegisterCompressor实现自定义的压缩方法。
注意,压缩算法应用于客户端和服务端两侧。
支持并发,从三个角度分析:
ClientConn
支持多个GoroutineSteams
中,SendMsg
/RecvMsg
可分别在两个Goroutine中运行,但任何一个方法运行在多个Goroutine上是不安全的Server
每个客户端的invoke会对应一个Server端的Goroutine类似Compression,可用encoding.RegisterCodec实现自定义的序列化方法。
用mock生成测试代码,详细可细看。
认证的相关选项,包括 TLS/OAuth2/GCE/JWT ,一般用前两者即可。
介绍了Metadata的使用,类比于HTTP/1.1的Header。
长连接的参数分为3类:
四个级别的log level,针对不同场景:
Info
用于debug问题Warning
排查非关键性的问题Error
gRPC调用出现无法返回到客户端的问题Fatal
导致程序无法恢复的致命问题使用默认的HTTP或HTTPS代理。
结合官方提供的错误码,用 status.New
或者 status.Error
创建错误。
服务端方法映射,跟着教程走即可。
值得一提的是,采用c++中的grpc_cli模块,可以查看指定端口暴露出来的服务详情。
版本演进,一般情况下每6周一个小版本,紧急修复会打补丁号。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili:https://space.bilibili.com/293775192
公众号:golangcoding
在第一部分,我们学习了gRPC的基本调用过程,这样我们对全局层面有了一定了解。接下来,我们将结合官方文档,继续深入学习、探索下去。
示例很简单,客户端和服务端都大致分为两步:
creds
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
中opts.creds
里第二步:在连接中进行认证
handleRawConn
useTransportAuthenticator
creds
实现的ServerHandshake
实现了认证。到这里,认证已经完成,不过我们可以再看看,认证信息是怎么传递的newHTTP2Transport
,保存到结构体http2Server
中的authInfo
,最后返回了一个Interface ServerTransport
serveStreams
,然后调用了 http2Server
的HandleStreams
方法,这时,我们大致可以猜测,auth在这里被用到了operateHeaders
,在这里被赋值到 pr.AuthInfo
里,并被保存到s的Context中peer.FromContext
,然而并没有地方应用,那认证的分析,就告一段落了这一块我们暂不深入源码,先了解使用时的特性
代码逻辑很直观,即处理后返回
代码的关键在于两个函数inRange
和 stream.Send
用一个for循环进行多次发送,stream.Recv()
实现了从服务端获取数据,当EOF时,才调用stream.SendAndClose
结束发送
将 SendAndClose
变为 Send
,其余基本不变。从这里可以看到,正常的关闭都是由服务端发起的。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili:https://space.bilibili.com/293775192
公众号:golangcoding
参考官方文档,进行部署并运行成功
存在三个冗余字段 XXX_NoUnkeyedLiteral
XXX_unrecognized
XXX_sizecache
这部分主要是兼容proto2的,我们暂时不用细究
传入一个 cc grpc.ClientConnInterface 客户端连接
可调用的方法为SayHello,其内部的method为”/helloworld.Greeter/SayHello”,也就是/{package}.{service}/{method}
,作为一个唯一的URI
需要自己实现一个SayHello的方法
其中有个 UnimplementedGreeterServer 的接口,可以嵌入到对应的server结构体中(有方法未实现时,会返回codes.Unimplemented)
这里pb.UnimplementedGreeterServer被嵌入了server结构,所以即使没有实现SayHello方法,编译也能通过。
但是,我们通常要强制server在编译期就必须实现对应的方法,所以生产中建议不嵌入。
1 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error){ |
1 | lis, err := net.Listen("tcp", port) |
因为gRPC的应用层是基于HTTP2的,所以这里不出意外,监听的是tcp端口
s.cv = sync.NewCond(&s.mu)
条件锁,用于关闭连接EnableTraciing
,会调用golang.org/x/net/trace 这个包对比自己创建的server和pb中定义的server,确定每个方法都已经实现
service放在 m map[string]*service
中,所以一个server可以放多个proto定义的服务
内部的method和stream放在 service 中的两个map中
新建一个conn连接,这里是一个支持HTTP2.0的客户端,暂不细讲
新建一个client,包装对应的method,方便调用SayHello
1 | r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) |
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的系列,会更具理论性。
代码实例
1 | func decorator(f func(s string)) func(s string) { |
一句话解释:在函数f前后,添加装饰性的功能函数,但不改变函数本身的行为。
这种设计模式,对一些被高频率调用的代码非常有用:
而装饰性的功能,常见的有:
代码示例
1 | type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc |
一句话解释:用不定参数的特性,将入参中的函数,逐个应用到对象上
看到这里,如果你能想起之前
Functional Option
那篇,会发现有这块的影子。
主要应用于: 有多种可选择的配置(对应Field)或处理(对应方法)的复杂对象。
耗子叔在后面又增加了一些用Goroutine+Channel的方式,其实就是讲Channel作为一个管道的承载体。
关于访问者设计者模式,我之前在Kubernetes源码分析中专门分析了源码。今天,我们也简单地过一下。
1 | // 定义访问的函数类型 |
然后看其中一个实现:NameVisitor,其余的也类似,这样就能注入对应的Visitor
1 | type NameVisitor struct { |
当然,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