GOPATH 与 Go Modules 的区别
1. 概念
- GOPATH
- 是 Go 的早期依赖管理机制。
- 所有的 Go 项目和依赖包必须放在
GOPATH目录中(默认是~/go)。 - 一定要设置 GO111MODULE=off
- 项目路径必须按照
src/包名的结构组织。 - 不支持版本控制,依赖管理需要手动处理(例如
go get)。 - 查找依赖包的顺序是 gopath/src 找,找到就去 goroot/src 目录下查找
- Go Modules
- 是 Go 1.11 引入的模块化依赖管理机制,默认在 Go 1.13 后启用。
- 不依赖
GOPATH,项目可以放在任意目录下。 - 每个项目有独立的
go.mod文件,用于管理依赖和版本。 - GO111MODULE=on 要开启
// vender
2. 依赖管理
- GOPATH
-
依赖包统一存储在
GOPATH的pkg目录中。 - 依赖版本不固定,例如运行
go get时会拉取最新版本,无法锁定具体版本。 - 缺乏模块化管理机制,多个项目可能出现依赖冲突。
- Go Modules
- 使用
go.mod和go.sum文件记录依赖和版本信息。 - 支持版本控制,明确指定依赖版本。
- 支持模块间的隔离,避免依赖冲突。
3. 项目组织
- GOPATH
-
项目必须位于
GOPATH/src目录下。 - 目录结构固定:
GOPATH/src/github.com/username/project。 - 一定要设置 GO11MODULE=off
- Go Modules
- 项目可以放在任何目录中,与
GOPATH无关。 - 更灵活,开发者可以自由选择目录结构。
4. 适用场景
- GOPATH
- 适用于老旧的 Go 项目或工具链。
- 适合简单的项目,不需要复杂的依赖管理。
- Go Modules
- 是 Go 的推荐标准,适用于现代项目。
- 对于需要依赖版本管理的项目,更加合适。
5. 命令区别
- GOPATH
-
go get:用于获取依赖。 -
go build:在GOPATH中查找依赖并构建。 - Go Modules
go mod init:初始化模块,生成go.mod文件。go mod tidy:清理和同步依赖。go mod vendor:将依赖下载到vendor目录。
总结
- 如果是新项目,应该尽量使用 Go Modules,因为它提供了更强大的功能和灵活性。
- 如果需要维护旧项目,可能会继续使用 GOPATH。
go 语言的编码规范
- 为什么需要代码规范
- 代码规范不是强制的,也就是说你不遵循代码规范写出来的代码运行也是完全没有问题的。
- 代码规范目的是方便团队形成一个统一的代码风格,提高代码的可读性、规范性和统一性。本规范将从命名规范、注释规范、代码风格和 Go 语言提供的常用的工具这几个方面做一个说明。
- 规范并不是唯一的,也就是说理论上每个公司都可以制定自己的规范,不过一般来说整体上规范差异不会很大。
2. 代码规范
1. 命名规范
命名是代码规范中很重要的一部分,统一的命名规则有利于提高代码的可读性。好的命名仅通过命名就可以获取到足够多的信息。
- a. 当命名(包括常量、变量、类型、函数名、结构字段等)以一个大写字母开头:
-
如:
Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包)。 -
这种称为导出(类似面向对象语言中的
public)。 - b. 命名如果以小写字母开头:
- 则对包外是不可见的,但是它们在整个包的内部是可见并且可用的(类似面向对象语言中的
private)。
1.1 包名:package
- 保持
package的名字和目录保持一致,尽量采取有意义的包名。 - 简短、有意义,尽量和标准库不冲突。
- 包名应该为小写单词,不要使用下划线或者混合大小写。
package model
package main
什么是 RPC
- RPC(Remote Procedure Call) 远程过程调用,简单理解是个节点请求另一个节点提供的服务
- 对应 rpc 的是本地过程调用,函数调用是最常见的过程调用
- 将本地过程调用变成远程过程调用会面临各种问题
- 原本的本地函数放到另一个服务器上运行。但是引入了很多新问题
- Call 的 id 映射
- 序列化和反序列化(重要)
- 网络传输(重要)
远程过程调用带来的新问题
- Call ID 映射
- 在本地调用中,函数体是直接通过函数表来指向的。
- 在远程调用中,所有的函数都必须有自己的唯一 ID。
- 客户端和服务端分别维护一个(函数 <--> Call ID)的对应表。
- 客户端调用时需要找到对应的 Call ID,服务端通过 Call ID 找到对应的函数。
- 序列化和反序列化
- 客户端需要将参数序列化为字节流,传递给服务端。
- 服务端接收字节流后反序列化为参数。
- 不同语言之间的调用需要统一的序列化协议。
- 网络传输
- 所有数据需要通过网络传输。
- 网络传输层需要将 Call ID 和序列化后的参数传递给服务端。
- 服务端处理后将结果返回客户端。
通过 http 来实现一个简单 rpc
// server
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)
func main() {
// path 相当于callID
// 返回格式:json {data:3}
// http://127.0.0.1:8000/add?a=1&b=2
// 网络传输协议
http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm() // 解析参数
fmt.Println("path:", r.URL.Path)
aList, aOK := r.Form["a"]
bList, bOK := r.Form["b"]
if !aOK || !bOK || len(aList) == 0 || len(bList) == 0 {
http.Error(w, `{"error":"missing parameter"}`, http.StatusBadRequest)
return
}
a, _ := strconv.Atoi(aList[0])
b, _ := strconv.Atoi(bList[0])
w.Header().Set("Content-Type", "application/json; charset=utf-8")
jData, _ := json.Marshal(map[string]int{
"data": a + b,
})
_, _ = w.Write(jData)
})
_ = http.ListenAndServe(":8000", nil)
}
client
// client
package main
import (
"encoding/json"
"fmt"
"github.com/kirinlabs/HttpRequest"
)
type ResponseData struct {
Data int `json:"data"`
}
// rpc远程过程调用,如何做到像本地调用
func Add(a, b int) int {
req := HttpRequest.NewRequest()
res, _ := req.Get(fmt.Sprintf("http://127.0.0.1:8000/%s?a=%d&b=%d", "add", a, b))
body, _ := res.Body()
//fmt.Println(string(body))
rspData := ResponseData{}
_ = json.Unmarshal(body, &rspData)
return rspData.Data
}
func main() {
//conn, err := net.Dial("tcp", "127.0.0.1:8000")
fmt.Println(Add(1, 2))
}
rpc 开发的四大要素
RPC 技术在架构设计上有四部分组成,分别是:客户端、客户端存根、服务端、服务端存根。
- 客户端 (Client):服务调用发起方,也称为服务消费者。
- 客户端存根 (Client Stub):该程序运行在客户端所在的计算机上,主要用来存储要调用的服务器的地址。另外,该程序还负责将客户端请求远程服务器程序的数据信息打包成数据包,通过网络发送给服务端 Stub 程序;其次,还要接收服务端 Stub 程序发送的调用结果数据包,并解析返回给客户端。
- 服务端 (Server):远端的计算机上运行的程序,其中有客户端要调用的方法。
- 服务端存根 (Server Stub):接收客户端 Stub 程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端 Stub 程序。
基于 go 语言包的 rpc(helloworld)
package main
import (
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type HelloService struct{}
func (s *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
return nil
}
func main() {
// 启动rpc服务
// 1. 实例化一个Server
// 2. 调用Server的Register方法注册rpc服务
// 3. 调用Server的Serve方法监听端口(启动服务)
listener, err1 := net.Listen("tcp", ":1234")
if err1 != nil {
return
}
// go 语言是有一个内置的rpc包的,可以用来实现rpc服务
err := rpc.RegisterName("HelloService", new(HelloService))
if err != nil {
return
}
for {
conn, er := listener.Accept()
if er != nil {
return
}
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) // 当一个新的连接进来的时候,rpc会处理这个连接
}
}
// python调用rpc服务
//import json
//import socket
//
//request = {
//"id":0,
//"params":["imooc"],
//"method":"HelloService.Hello"
//}
//client = socket.create_connection(("localhost", 1234))
//client.send(json.dumps(request).encode('utf-8'))
//
//# 获取服务器返回的数据
//rsp = client.recv(1024)
//rsp = json.loads(rsp.decode('utf-8'))
//print(rsp)
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
func main() {
// 连接rpc服务
connn, err := net.Dial("tcp", "localhost:1234") // 连接rpc服务
if err != nil {
panic("连接失败")
}
var reply string
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(connn))
err = client.Call("HelloService.Hello", "json grpc", &reply)
if err != nil {
panic("调用失败")
}
fmt.Println(reply)
}
能不能监听 http 请求呢
package main
import (
"io"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
)
type HelloService struct{}
func (s *HelloService) Hello(request string, reply *string) error {
// 返回值是通过修改replay的值
*reply = "Hello, " + request
return nil
}
func main() {
_ = rpc.RegisterName("HelloService", new(HelloService))
http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
Writer: w,
ReadCloser: r.Body,
}
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":1234", nil)
}
request = {
"id": 0,
"params": ["bobby"],
"method": "HelloService.Hello"
}
import requests
rsp = requests.post("http://localhost:1234/jsonrpc", json=request)
print(rsp.text)
重点看我代码中 new_helloworld
- handler 处理业务逻辑
package handler
const HelloServiceName = "handler/HelloService"
// 服务端的业务逻辑
type NewHelloService struct{}
// 业务逻辑
func (s *NewHelloService) Hello(request string, reply *string) error {
// 返回值是通过修改replay的值
*reply = "Hello, " + request
return nil
}
- client 通过 client_proxy 来发起
// client
package main
import (
"fmt"
"RpcLearn/new_helloworld/client_proxy"
)
func main() {
// 建立连接
client := client_proxy.NewHelloServiceClient("tcp", "127.0.0.1:1234")
var reply string
err := client.Hello("bobby", &reply)
if err != nil {
fmt.Println(err)
panic(err)
}
fmt.Println(reply)
}
// client_proxy
package client_proxy
import (
"net/rpc"
"RpcLearn/new_helloworld/handler"
)
type HelloServiceStub struct {
*rpc.Client
}
// 在go中没有类、对象,就意味着没有初始化方法
func NewHelloServiceClient(protocol, address string) *HelloServiceStub {
conn, err := rpc.Dial(protocol, address)
if err != nil {
panic("dial error")
}
return &HelloServiceStub{conn}
}
func (c *HelloServiceStub) Hello(request string, reply *string) error {
err := c.Client.Call(handler.HelloServiceName+".Hello", request, reply)
if err != nil {
return err
}
return err
}
- server 通过 server_proxy 来响应
// server
package main
import (
"RpcLearn/new_helloworld/handler"
"net"
"net/rpc"
"RpcLearn/new_helloworld/server_proxy"
)
func main() {
// 1. 实例化server
listener, err := net.Listen("tcp", ":1234")
//2. 注册处理逻辑
//_ = rpc.RegisterName(handler.HelloServiceName, new(handler.NewHelloService))
err = server_proxy.RegisterHelloService(new(handler.NewHelloService))
if err != nil {
return
}
//3. 启动服务
for {
conn, _ := listener.Accept() // 当一个新的连接进来的时候,
go rpc.ServeConn(conn)
}
}
// server_proxy
package server_proxy
import (
"RpcLearn/new_helloworld/handler"
"net/rpc"
)
type HelloServicer interface {
Hello(request string, reply *string) error
}
// 如何做到解耦呢,我们关心的是函数 鸭子类型
func RegisterHelloService(srv HelloServicer) error {
return rpc.RegisterName(handler.HelloServiceName, srv)
}
- 这些概念在 grpc 中都有对应
- 发自灵魂的拷问 server_proxy 和 client_proxy 能否自动生成,为多种语言生成
- 都能满足就是 grpc+protobuf
go install 与 go get 的主要区别总结
| 功能 | go install |
go get |
|---|---|---|
| 用途 | 安装命令行工具 | 管理依赖包 |
| 文件更改 | 不修改 go.mod |
修改 go.mod 和 go.sum |
| 模块支持 | 支持模块和版本 | 主要用于管理模块 |
| 推荐场景 | 安装工具,如 protoc-gen-go |
引入或更新依赖库 |
| Go 1.17+ 建议 | 用于工具安装 | 不再推荐用于工具安装 |
protoc 的使用
# 先安装protoc
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 安装protobuf support
syntax="proto3";
package helloworld;
option go_package = ".";
message HelloRequest {
string name = 1; // 1 是编号不是值
int32 age = 2; // 2 是编号不是值
}
# 生成普通的 .pb.go 文件(用于消息结构定义):
protoc --go_out=. --go_opt=paths=source_relative helloworld.proto
# 生成 gRPC 的 .grpc.pb.go 文件:
protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld.proto
# protoc --go_out=. helloworld.proto 我在proto这个目录下直接执行
和 json 的对比
package main
import (
"encoding/json"
"fmt"
"github.com/golang/protobuf/proto"
"learngo/proto"
)
// 结构休的对比
type Hello struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
req := helloworld.HelloRequest{
Name: "gRPC",
Age: 18,
}
jsonStruct := Hello{
Name: req.Name,
Age: int(req.Age),
}
jsonRep, _ := json.Marshal(jsonStruct)
fmt.Println(len(jsonRep))
rsp, _ := proto.Marshal(&req) // 具体的编码是如何做到的 那大可以自行学习
newReq := helloworld.HelloRequest{}
proto.Unmarshal(rsp, &newReq)
fmt.Println(newReq.Name, newReq.Age)
fmt.Println(len(rsp))
}
stub 没用生成(存根)
给 proto 添加一些方法的声明,编译时就要添加 grpc 的出现参数
protoc -I . --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld.proto # 这里没有用到 --go-grpc_opt=paths=source_relative
# 这个命令会额外生成grpc调用的一些内容
# 这里要注意生成 protoc --go_out=. helloworld.proto 不要删除它们两两个不冲突
protoc 尽量使用最新的
syntax="proto3";
option go_package = ".:proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
protoc -I helloworld.proto --go_out=. --go-grpc_out=.
protoc -I . --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative helloworld.proto
# 是的,新版本的 protoc 工具将生成的文件分为两个部分:
# Protobuf 本身的定义文件(.pb.go):
# 生成普通的 Protobuf 消息定义代码。
# 包含消息的结构体、枚举等内容。
# 使用 --go_out 选项生成。
# 支持 gRPC 的服务定义文件(_grpc.pb.go):
# 生成与 gRPC 服务相关的代码。
# 包括服务接口、客户端代码和服务端代码。
# 使用 --go-grpc_out 选项生成。
# 原因:
# 这种拆分更清晰,允许你在不使用 gRPC 的情况下单独使用 Protobuf 消息定义。
# 提高了灵活性和扩展性。例如,你可以仅使用 Protobuf 的定义,而不依赖 gRPC 的功能。
server.go
package main
import (
"context"
"google.golang.org/grpc"
"net"
"learngo/grpc_test/proto"
)
type Server struct {
proto.UnimplementedGreeterServer // 嵌入 UnimplementedGreeterServer
}
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
//注意定义的时候name是小写的,编译完成后是大小的,所以这里可以直接调用
return &proto.HelloReply{Message: "Hello " + request.Name}, nil
}
func main() {
// 实例化grpc server
g := grpc.NewServer()
// 注册HelloService
proto.RegisterGreeterServer(g, &Server{})
// 监听端口
lis, err := net.Listen("tcp", "0.0.0.0:1234")
if err != nil {
panic(err)
}
// 启动服务
err = g.Serve(lis)
if err != nil {
panic("failed to serve: " + err.Error())
}
}
client.go
package main
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"learngo/grpc_test/proto"
)
func main() {
// 创建一个带超时的上下文
_, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// 使用 grpc.DialContext 连接服务端
conn, err := grpc.NewClient(
"localhost:1234", // 服务端地址
grpc.WithTransportCredentials(insecure.NewCredentials()), // 非安全连接(开发环境使用)
)
if err != nil {
panic(fmt.Sprintf("Failed to connect to server: %v", err))
}
defer conn.Close()
// 创建 gRPC 客户端
client := proto.NewGreeterClient(conn)
// 调用服务方法
resp, err := client.SayHello(context.Background(), &proto.HelloRequest{Name: "World"})
if err != nil {
panic(fmt.Sprintf("Failed to call SayHello: %v", err))
}
fmt.Println(resp.Message) // 输出服务器返回的消息
}
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4770
