Go工程师体系课 002【学习笔记】

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
  • 依赖包统一存储在 GOPATHpkg 目录中。
  • 依赖版本不固定,例如运行 go get 时会拉取最新版本,无法锁定具体版本。
  • 缺乏模块化管理机制,多个项目可能出现依赖冲突。
  • Go Modules
  • 使用 go.modgo.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 语言的编码规范

  1. 为什么需要代码规范
  2. 代码规范不是强制的,也就是说你不遵循代码规范写出来的代码运行也是完全没有问题的。
  3. 代码规范目的是方便团队形成一个统一的代码风格,提高代码的可读性、规范性和统一性。本规范将从命名规范、注释规范、代码风格和 Go 语言提供的常用的工具这几个方面做一个说明。
  4. 规范并不是唯一的,也就是说理论上每个公司都可以制定自己的规范,不过一般来说整体上规范差异不会很大。

2. 代码规范

1. 命名规范

命名是代码规范中很重要的一部分,统一的命名规则有利于提高代码的可读性。好的命名仅通过命名就可以获取到足够多的信息。

  • a. 当命名(包括常量、变量、类型、函数名、结构字段等)以一个大写字母开头
  • 如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包)。
  • 这种称为导出(类似面向对象语言中的 public)。
  • b. 命名如果以小写字母开头
  • 则对包外是不可见的,但是它们在整个包的内部是可见并且可用的(类似面向对象语言中的 private)。

1.1 包名:package

  • 保持 package 的名字和目录保持一致,尽量采取有意义的包名。
  • 简短、有意义,尽量和标准库不冲突。
  • 包名应该为小写单词,不要使用下划线或者混合大小写。
package model
package main

什么是 RPC

  1. RPC(Remote Procedure Call) 远程过程调用,简单理解是个节点请求另一个节点提供的服务
  2. 对应 rpc 的是本地过程调用,函数调用是最常见的过程调用
  3. 将本地过程调用变成远程过程调用会面临各种问题
  4. 原本的本地函数放到另一个服务器上运行。但是引入了很多新问题
  5. Call 的 id 映射
  6. 序列化和反序列化(重要)
  7. 网络传输(重要)

远程过程调用带来的新问题

  1. Call ID 映射
  2. 在本地调用中,函数体是直接通过函数表来指向的。
  3. 在远程调用中,所有的函数都必须有自己的唯一 ID。
  4. 客户端和服务端分别维护一个(函数 <--> Call ID)的对应表。
  5. 客户端调用时需要找到对应的 Call ID,服务端通过 Call ID 找到对应的函数。
  6. 序列化和反序列化
  7. 客户端需要将参数序列化为字节流,传递给服务端。
  8. 服务端接收字节流后反序列化为参数。
  9. 不同语言之间的调用需要统一的序列化协议。
  10. 网络传输
  11. 所有数据需要通过网络传输。
  12. 网络传输层需要将 Call ID 和序列化后的参数传递给服务端。
  13. 服务端处理后将结果返回客户端。

通过 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 程序。

stub.png

基于 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 installgo get 的主要区别总结

功能 go install go get
用途 安装命令行工具 管理依赖包
文件更改 不修改 go.mod 修改 go.modgo.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

(0)
Walker的头像Walker
上一篇 2025年11月25日 02:00
下一篇 2025年11月25日 00:00

相关推荐

  • Node深入浅出(圣思园教育) 002【学习笔记】

    node 的包管理机制和加载机制 npm search xxxnpm view xxxnpm install xxx nodejs 文件系统操作的 api Node.js 的 fs 模块提供同步(Sync)与基于回调/Promise 的异步 API,可以操作本地文件与目录。日常开发中常用的能力包括读取、写入、追加、删除、遍历目录、监听变化等。以下示例基于 C…

    个人 2025年11月24日
    6300
  • TS珠峰 001【学习笔记】

    课程大纲 搭建 TypeScript 开发环境。 掌握 TypeScript 的基础类型,联合类型和交叉类型。 详细类型断言的作用和用法。 掌握 TypeScript 中函数、类的类型声明方式。 掌握类型别名、接口的作用和定义。 掌握泛型的应用场景,熟练应用泛型。 灵活运用条件类型、映射类型与内置类型。 创建和使用自定义类型。 理解命名空间、模块的概念已经使…

    个人 2025年3月27日
    1.3K00
  • 深入理解ES6 011【学习笔记】

    Promise与异步编程 因为执行引擎是单线程的,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列中,每当一段代码准备执行时,都会被添加到任务队列中,每当引擎中的一段代码结束执行,事件循环会执行队列中的一下个任务。 Promise相当于异步操作结果占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise,就像这样…

    个人 2025年3月8日
    96200
  • 向世界挥手,拥抱无限可能 🌍✨

    站得更高,看到更远 生活就像一座座高楼,我们不断向上攀登,不是为了炫耀高度,而是为了看到更广阔的风景。图中的两位女孩站在城市之巅,伸展双手,仿佛在迎接世界的无限可能。这不仅是一次俯瞰城市的旅程,更是对自由和梦想的礼赞。 勇敢探索,突破边界 每个人的生活都是一场冒险,我们生而自由,就该去探索未知的风景,去经历更多的故事。或许路途中会有挑战,但正是那些攀爬的瞬间…

    个人 2025年2月26日
    1.1K00
  • Go工程师体系课 protobuf_guide【学习笔记】

    Protocol Buffers 入门指南 1. 简介 Protocol Buffers(简称 protobuf)是 Google 开发的一种语言无关、平台无关、可扩展的结构化数据序列化机制。与 JSON、XML 等序列化方式相比,protobuf 更小、更快、更简单。 项目主页:https://github.com/protocolbuffers/prot…

    个人 2025年11月25日
    1.1K00

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
欢迎🌹 Coding never stops, keep learning! 💡💻 光临🌹