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

相关推荐

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

    订单及购物车 先从库存服务中将 srv 的服务代码框架复制过来,查找替换对应的名称(order_srv) 加密技术基础 对称加密(Symmetric Encryption) 原理: 使用同一个密钥进行加密和解密 就像一把钥匙,既能锁门也能开门 加密速度快,适合大量数据传输 使用场景: 本地文件加密 数据库内容加密 大量数据传输时的内容加密 内部系统间的快速通…

    个人 2025年11月25日
    15700
  • Go工程师体系课 006【学习笔记】

    项目结构说明:user-web 模块 user-web 是 joyshop_api 工程中的用户服务 Web 层模块,负责处理用户相关的 HTTP 请求、参数校验、业务路由以及调用后端接口等功能。以下是目录结构说明: user-web/ ├── api/ # 控制器层,定义业务接口处理逻辑 ├── config/ # 配置模块,包含系统配置结构体及读取逻辑 …

    个人 2025年11月25日
    17500
  • 深入理解ES6 011【学习笔记】

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

    个人 2025年3月8日
    1.1K00
  • TS珠峰 001【学习笔记】

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

    个人 2025年3月27日
    1.5K00
  • TS珠峰 002【学习笔记】

    泛型 /* * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git &a…

    个人 2025年3月27日
    1.5K00

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

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

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