Go工程師體系課 002

9次閱讀

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

Go 工程師體系課 002

基於 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) // 輸出服務器返回的消息
}
正文完
 0