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

相关推荐

  • 深入理解ES6 010【学习笔记】

    改进的数组功能 new Array()的怪异行为,当构造函数传入一个数值型的值,那么数组的length属性会被设为该值;如果传入多个值,此时无论这些值是不是数值型的,都会变为数组的元素。这个特性另人困惑,你不可能总是注意传入数据的类型,所以存在一定的风险。 Array.of() 无论传多少个参数,不存在单一数值的特例(一个参数且数值型),总是返回包含所有参数…

    个人 2025年3月8日
    1.2K00
  • 深入理解ES6 005【学习笔记】

    解构:使用数据访问更便捷 如果使用var、let或const解构声明变量,则必须要提供初始化程序(也就是等号右侧的值)如下会导致错误 // 语法错误 var {tyep,name} // 语法错误 let {type,name} // 语法错误 const {type,name} 使用解构给已经声明的变量赋值,哪下 let node = { type:&qu…

    个人 2025年3月8日
    1.2K00
  • 深入理解ES6 012【学习笔记】

    代理(Proxy)和反射(Reflection)API 代理是一种可以拦截并改变底层javascript引擎操作的包装器,在新语言中通过它暴露内部运作对象,从而让开发者可以创建内建的对象。 代理陷阱 覆写的特性 默认特性 get 读取一个属性值 Reflect.get() set 写入一个属性值 Reflect.set() has in操作符 Reflect…

    个人 2025年3月8日
    1.1K00
  • Nuxt3_扫盲 入门与原理介绍【学习笔记】

    Nuxt 3 入门与原理介绍 💡 什么是 Nuxt 3? Nuxt 3 是基于 Vue 3 和 Vite 打造的全栈前端框架,支持: 服务端渲染(SSR) 静态站点生成(SSG) 单页应用(SPA) 构建全栈应用(支持 API) Nuxt 3 是 Vue 的“加强版”,帮你简化项目结构和开发流程。 🔧 核心原理 功能 Nuxt 如何处理 ✅ 页面路由 自动根…

    个人 2025年4月6日
    2.1K00
  • Go工程师体系课 006【学习笔记】

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

    个人 2025年11月25日
    17300

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

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

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