目录
- 基础
- 环境
- 包管理
- 编码规范
- 命名规范
- 注释
- import 规范
- 错误处理
- RPC
- 内置 RPC
- 改协议
- 改调用
基础
- 基础部分参考这个系列
- 接下来的这部分是对上面的更新和重构,更加深入理解框架部分
环境
- 基础环境,主要在Linux上搞;最主要是 docker,docker-compose 和 node,记得 docker 要配置阿里云镜像加速器
# docker curl -fsSL https://get.docker.com bash -s docker --mirror Aliyun sudo systemctl start docker sudo systemctl enable docker sudo usermod -aG docker ${USER} sudo chmod a+rw /var/run/docker.sock sudo systemctl restart docker docker run hello-world
# mysql docker run -p 3306:3306 --name mymysql -v $PWD/conf:/etc/mysql/conf.d -v $PWD/logs:/logs -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 mysql -uroot -p # 授权访问 GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' IDENTIFIED BY 'root' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY 'root' WITH GRANT OPTION; FLUSH PRIVILEGES;
- centos7上安装 node,推荐使用16.16.0版本,太高会报错
包管理
- 说明一下 go path 和现在 go module 之间的差异
- GOPATH
- 用
go env
能看到,这个需要自己设置,比如D:\Go\workspace;
- 那么,我们新建的项目都要放在 D:\Go\workspace\src 下面
- 同时,要设置
GO111MODULE=off
,用go env -w
- 不设置的话会去
GOROOT
下面找,就是安装目录D:\Go\src
下的,放标准库的 - 也就是说,这种包管理方式就是:不做包管理,对开发go的人容易,对go开发者很头疼
- 用
- go module
- 设置
GO111MODULE=on
- 在 GoLand 新建项目会自动新建
go.mod
文件,有这么两行module goModProject go 1.17
- import 未下载的包会自动下载并管理,可以多版本
- 设置
编码规范
- 代码规范不是强制的,⽬的是⽅便团队形成⼀个统⼀的代码⻛格,提高可读性;规范并不是唯⼀的
命名规范
- 统⼀的命名规则有利于提⾼的代码的可读性,仅通过命名就可以获取到⾜够多的信息
- 以大写字⺟开头(常量、变量、类型、函数名、结构体等),代表可以被外部包的代码所使用,也被称为导出
- 以小写字⺟开头,则对包外是不可见的,类似 Java 的 private
- 包名,package
- 保持 package 的名字和⽬录⼀致,尽量采取有意义的包名,和标准库不要冲突
- 使用小写单词,不要使⽤下划线或者混合大小写
- 文件名
- 应该为小写单词,使⽤下划线分隔各个单词
- 结构体
- 采用驼峰命名法,首字母根据访问控制⼤写或者小写
type User struct{ Username string Email string } u := User{ Username: "Roy", Email: "roy.yang@gmail.com", }
- 采用驼峰命名法,首字母根据访问控制⼤写或者小写
- 接口
- 和结构体类似
- 单个函数的接口名以
er
作为后缀type Reader interface { Read(p []byte) (n int, err error) }
- 变量
- ⼀般遵循驼峰法
- 遇到特有名词时(API,ID等),需要遵循以下规则
- 如果变量为私有,且特有名词为首个单词,则使用小写,如
apiClient
- 其它情况都应当使⽤该名词原有的写法,如 APIClient、repoID、UserID;错误示例:UrlArray
- 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
- 如果变量为私有,且特有名词为首个单词,则使用小写,如
- 常量命名
- 常量均需使⽤全部⼤写字⺟组成,并使⽤下划线分词
- 如果是枚举类型的常量,需要先创建相应类型
const APP_VER = "1.0.0" type Scheme string const ( HTTP Scheme = "http" HTTPS Scheme = "https" )
注释
- 清晰的注释非常重要
- 虽然我们自己不喜欢写注释,但是很讨厌不写注释的人
- go 的注释是 C++ 风格,用
//
单行注释和/**/
多行注释 - go 语⾔⾃带的 godoc ⼯具可以根据注释生成⽂档
- 包注释
- 每个包都应该有⼀个包注释,⼀个位于 package 子句之前的块注释或行注释
- 应该包含
/* 包的基本简介(包名,简介) 创建者,格式: 创建⼈: rtx 名 创建时间,格式:创建时间: yyyyMMdd */
- 结构体/接口注释
- 结构体名, 结构体说明
- 结构体内的每个成员变量都要有说明,该说明放在成员变量的后⾯
- 函数注释
- 应包括三个方面
/* 简要说明,格式说明:以函数名开头,“,” 分隔说明部分 参数列表:每⾏⼀个参数,参数名开头,“,” 分隔说明部分 返回值: 每⾏⼀个返回值 */
- 上面说的这些注释都是写在上面
- 应包括三个方面
- 代码注释
- ⼀些关键位置的代码逻辑,或者局部较为复杂的逻辑,需要有相应的说明
- 注释风格
- 中英⽂字符之间严格使⽤空格分隔
import 规范
- 包分三类,标准库包,程序内部包,第三⽅包
- 建议采用如下顺序
import ( "encoding/json" "strings" "github.com/astaxie/beego" "github.com/go-sql-driver/mysql" "myproject/models" "myproject/controller" "myproject/utils" )
- 在项⽬中不要使⽤相对路径引⼊第三方包,本项目自己的包建议用相对路径
错误处理
- 不能丢弃任何返回 err 的调⽤,不要使⽤
_
丢弃,必须全部处理 - 接收到错误,要么
return err
,要么用 log 记录下来 - 尽量不要用
panic
,除非你知道你在做什么 - 采⽤独⽴的错误流进⾏处理
// 错误写法 if err != nil { // error handling } else { // normal code } // 正确写法 if err != nil { // error handling return // or continue }
RPC
- remote procedure call 一个节点请求另一个节点的服务,一般分布式应用才会用到
- 将本地过程调用变为远程过程调用要面临的问题,我们可以类比本地调用过程
- Call 的 ID 映射
- 序列化和反序列化(json/xml/protobuf/msgpack)
- 网络传输(gin/beego/net)
- 前面解释过这些概念和流程,这里再举个例子回顾一下
- 先看一个简单的RPC流程
- 调用端和服务端都要经历序列化和反序列化
- 我们选择
protobuf
作为序列化协议,更为通用;不同语言之间只要遵守了协议,也能交流 - json 序列化主要是效率不够高,一般只在 gin 和浏览器之间使用,但我们的服务不止在这两个地方
- 网络传输也是必不可少的,我们需要建立 http 连接,在 grpc 中使用
http2.0
避免连接断开的问题;我们也可以自己封装一层协议
- 先看一个简单的RPC流程
- 使用 RPC
- 封装一个简单的 RPC 流程
- server 端,使用 http 启动服务
// server/main.go /* 1.定义处理逻辑(反序列化,序列化) 2.启动服务,监听端口 */ package main import ( "encoding/json" "fmt" "net/http" "strconv" ) func main() { // http://127.0.0.1:8000?a=1 http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() // 解析参数 fmt.Println("path:", r.URL.Path) a, _ := strconv.Atoi(r.Form["a"][0]) // 搞成int w.Header().Set("Content-Type", "application/json") d, _ := json.Marshal(map[string]int{"data": a}) // 序列化(格式化) _, _ = w.Write(d) }) err := http.ListenAndServe(":8088", nil) if err != nil { fmt.Println(err) } }
- client 端,网络传输:http,序列化:json,ID:/add
// client/main.go /* 1.建立连接 2.序列化数据 3.发起请求 4.反序列化返回值 */ package main import ( "encoding/json" "fmt" "github.com/kirinlabs/HttpRequest" ) type RespData struct { Data int `json:"data"` // 使用json解析 } func Add(a int) int { req := HttpRequest.NewRequest() res, _ := req.Get(fmt.Sprintf("http://127.0.0.1:8088/%s?a=%d", "add", a)) body, _ := res.Body() //fmt.Println(string(body)) // {"data":1} r := RespData{} _ = json.Unmarshal(body, &r) return r.Data } func main() { fmt.Println(Add(10)) }
- 看起来和普通的单体应用前后端请求没啥区别
- 因为这里在本地请求,主要是体会 RPC 的几个要点,后面 grpc 框架会帮我们解决这些问题
- RPC 框架开发要素
- 技术架构上有四部分:客户端、客户端存根、服务端、服务端存根
- 存根(stub)干大部分的活
- 该程序运⾏在客户端所在的计算机机器上,主要⽤来存储要调⽤的服务器的地址
- 另外,还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过⽹络发送给服务端Stub 程序
- 其次,还要接收服务端 Stub 程序发送的调⽤结果数据包,并解析返回给客户端
- 具体开发中,我们都会使用动态代理技术,⾃动⽣成的 Stub 程序
- 因为地址、数据信息等可能会修改,stub 文件也可能会非常多,靠人写估计会辞职
- 技术架构上有四部分:客户端、客户端存根、服务端、服务端存根
内置 RPC
- 尝试使用 go 语言内置 RPC,记得要和上面的简单过程对比
- server 端
package main import ( "net" "net/rpc" ) // class type HelloService struct { } // class function 定义处理逻辑 func (s *HelloService) Hello(request string, reply *string) error { // 通过修改指针地址返回 *reply = "hello" + request // 注:这里的 *reply 是不能分配空间的,调用时必须实例化 return nil } func main() { // 1.实例化server listen, _ := net.Listen("tcp", ":1234") // 2.注册处理逻辑 _ = rpc.RegisterName("HelloService", &HelloService{}) // 3.启动服务,监听端口 conn, _ := listen.Accept() rpc.ServeConn(conn) }
- client 端
package main import ( "fmt" "net/rpc" ) func main() { // 1. 建立连接 client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { panic(err) } // 2. 发起请求 // var reply = new(string) // new会实例化空间并返回指针,通过指针reply返回数据,这里必须分配空间,不能传nil,或者说不能直接写成 var reply *string var reply string // 或者直接用 string 类型,它是有默认值的,或者说有地址,不是 nil err = client.Call("HelloService.Hello", "roy", &reply) if err != nil { panic("调用失败") } fmt.Println(reply) // 传用 &,取用 * }
- 上面的过程明显简单了很多,本质是 rpc 把 ID 和序列化的操作搞定了,无需我们关心底层细节
改协议
- 上面的调用有些怪异,但 go 是静态语言,不像 python 能直接
client.Hello()
- 我们可以跨语言发起请求,但要先更换协议
- go 用的是 Gob 协议,换成 json
// server package main import ( "net" "net/rpc" "net/rpc/jsonrpc" ) // class type HelloService struct { } // class function func (s *HelloService) Hello(request string, reply *string) error { // 通过修改指针地址返回 *reply = "hello" + request // 注:这里的 *reply 是不能分配空间的,调用时必须实例化 return nil } func main() { // 实例化server listen, _ := net.Listen("tcp", ":1234") // 注册处理逻辑 _ = rpc.RegisterName("HelloService", &HelloService{}) // 启动服务,监听端口 for { conn, _ := listen.Accept() // 使用 json 序列化 go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) // 使用协程 } }
// client package main import ( "fmt" "net" "net/rpc" "net/rpc/jsonrpc" ) func main() { // 1. 建立连接 conn, err := net.Dial("tcp", "localhost:1234") // 不用 rpc 了,否则还是 Gob 协议 if err != nil { panic(err) } // 2. 发起请求 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) // 使用 json 序列化 var reply string err = client.Call("HelloService.Hello", "roy", &reply) if err != nil { panic("调用失败") } fmt.Println(reply) }
- 接下来用 python 发起请求
- 这里可以用 socket,特点是每次调用就会发一次消息(等待套接字处理),不像 requests 底层是 HTTP 协议需要建立连接
- 方便起见我们还是用 requests,先将 go 端的 server 改为使用 http 协议,并启动
package main import ( "io" "net/http" "net/rpc" "net/rpc/jsonrpc" ) // class type HelloService struct { } // class function func (s *HelloService) Hello(request string, reply *string) error { // 通过修改指针地址返回 *reply = "hello" + request // 注:这里的 *reply 是不能分配空间的,调用时必须实例化 return nil } func main() { // 1.实例化server _ = rpc.RegisterName("HelloService", &HelloService{}) // 将 rpc 的传输协议改为 http;先不用理解,照猫画虎 http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) { var conn io.ReadWriteCloser = struct { io.Writer io.ReadCloser }{ ReadCloser: r.Body, Writer: w, } // ServeRequest is like ServeCodec but synchronously serves a single request. // It does not close the codec upon completion. rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) http.ListenAndServe(":1234", nil) }
- python 端
import requests # 这些key名称是固定的 req = { "id": 0, "params": ["Roy"], "method": "HelloService.Hello" } rsp = requests.post("http://localhost:1234/jsonrpc", json=req) print(rsp.text) # {"id":0,"result":"helloRoy","error":null}
- 但还是没有实现上面说的
client.Hello()
形式的调用,可以参考这段代码理解import zerorpc c = zerorpc.Client() c.connect("tcp://127.0.0.1:1234") print(c.Hello("Roy"))
- 在 python 中有一个魔法方法
__getattr__
,即使 client 端没有 Hello 方法的定义,也能根据方法名称构建网络请求,可以进入源码查看
- 上面改了序列化协议、传输协议,在 python 端尝试调用
改调用
- 我们回到 go,它没有 python 的高级特性,只能自定义;从这里开始,我们朝 RPC 框架逼近了
- 结构分析
- 首先要有一个 handler 模块,因为 client 和 server 可能不在一个服务器,总不能为了运行 client 把 server 代码拿过去,将处理逻辑放在 handler,大家各自带上
// handler/handler.go package handler // 名称冲突问题 // 相当于 ID,server_proxy 中会注册这个 name,client_proxy 中会根据这个 ID 调用注册的 Hello 方法 const HelloServiceName = "handler/HelloService" type NewHelloService struct { } func (s *NewHelloService) Hello(req string, reply *string) error { *reply = "Hello, " + req return nil }
- client 想直接调用 Hello,还需要一层封装,我们称为 client_proxy;主要作用是与 server 建立连接,封装出
Hello
方法给 clientpackage client_proxy import ( "goModProject/helloworld/handler" "net/rpc" ) // 所有方法,归在 Stub type HelloServiceStub struct { *rpc.Client } // 这里是连接 // go 语言没有类,没有初始化方法 func NewHelloServiceClient(protocol, address string) HelloServiceStub { conn, err := rpc.Dial(protocol, address) if err != nil { panic("Connect Error!") } return HelloServiceStub{conn} } // 这里是调用具体的逻辑,Hello方法 func (c *HelloServiceStub) Hello(req string, reply *string) error { // serviceMethod: Hello,对,就是 .Hello 调用 err := c.Call(handler.HelloServiceName+".Hello", req, reply) if err != nil { return err } return nil }
- 同样,将注册逻辑放在 server_proxy,并使用接口作为参数解耦
// server_proxy.go package server_proxy import ( "goModProject/helloworld/handler" "net/rpc" ) // 在服务端注册处理逻辑 // 这么写耦合太紧了,proxy 受限于 handler //func RegisterHelloService(srv handler.NewHelloService) error { // return rpc.RegisterName(handler.HelloServiceName, srv) //} // 使用接口解耦 type HelloService interface { Hello(request string, reply *string) error } // 使用接口作为参数,只要传入的结构体实现了 Hello 方法即可,不需要指定 handler.NewHelloService // 即使 handler 中的结构体名字变了也不需要改这里 // server 和 handler 中对上即可 func RegisterHelloService(srv HelloService) error { return rpc.RegisterName(handler.HelloServiceName, srv) }
// server.go package main import ( "goModProject/helloworld/handler" "goModProject/helloworld/server_proxy" "net" "net/rpc" ) func main() { // 实例化server listen, _ := net.Listen("tcp", ":1234") // 注册处理逻辑 _ = server_proxy.RegisterHelloService(&handler.NewHelloService{}) // 启动服务,监听端口 for { conn, _ := listen.Accept() rpc.ServeConn(conn) } }
- 首先要有一个 handler 模块,因为 client 和 server 可能不在一个服务器,总不能为了运行 client 把 server 代码拿过去,将处理逻辑放在 handler,大家各自带上
- 上面的过程一定要研究明白,主要是结构上优化,定义了框架的四要素;底层三问题还是使用 RPC 解决
- 问题也来了,如果所有的 Stub 都自己写,会痛苦不堪的
- 但我们发现,两个 proxy 的内容其实是有规律的,也就意味着可以自动生成
- 需要用到一个工具:
protoc
- 后面从 grpc 开始介绍,和我们这部分对比就会发现,过程一样