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,那麼使用這種形式的識別符號(identifier)的物件就可以被外部套件的程式碼所使用(客戶端程式需要先匯入這個套件)。
  • 這種稱為匯出(類似物件導向語言中的 public)。
  • b. 命名如果以小寫字母開頭
  • 則對套件外是不可見的,但是它們在整個套件的內部是可見且可用的(類似物件導向語言中的 private)。

1.1 套件名稱:package

  • 保持 package 的名稱和目錄保持一致,盡量採取有意義的套件名稱。
  • 簡短、有意義,盡量和標準函式庫不衝突。
  • 套件名稱應該為小寫單字,不要使用底線或者混合大小寫。
package model
package main

什麼是 RPC

  1. RPC (Remote Procedure Call) 遠端程序呼叫,簡單理解是一個節點請求另一個節點提供的服務。
  2. 對應 RPC 的是本地程序呼叫,函式呼叫是最常見的程序呼叫。
  3. 將本地程序呼叫變成遠端程序呼叫會面臨各種問題。
  4. 原本的本地函式放到另一個伺服器上運行。但是引入了很多新問題。
  5. 呼叫 ID (Call ID) 映射
  6. 序列化和反序列化 (Serialization and Deserialization) (重要)
  7. 網路傳輸 (Network Transmission) (重要)

遠端程序呼叫帶來的問題

  1. 呼叫 ID (Call ID) 映射
  2. 在本地呼叫中,函式主體是直接透過函式表來指向的。
  3. 在遠端呼叫中,所有的函式都必須有自己唯一的 ID。
  4. 客戶端和伺服端分別維護一個(函式 <--> 呼叫 ID)的對應表。
  5. 客戶端呼叫時需要找到對應的呼叫 ID,伺服端透過呼叫 ID 找到對應的函式。
  6. 序列化和反序列化 (Serialization and Deserialization)
  7. 客戶端需要將參數序列化為位元組流,傳遞給伺服端。
  8. 伺服端接收位元組流後反序列化為參數。
  9. 不同語言之間的呼叫需要統一的序列化協定。
  10. 網路傳輸 (Network Transmission)
  11. 所有資料需要透過網路傳輸。
  12. 網路傳輸層需要將呼叫 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)伺服端 (Server)伺服端存根 (Server Stub)

  • 客戶端 (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_proxyclient_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:

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4770

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

相關推薦

  • 深入理解ES6 005【學習筆記】

    解構:使用資料存取更便捷 如果使用 var、let 或 const 解構宣告變數,則必須要提供初始化程式(也就是等號右側的值)如下會導致錯誤 // 語法錯誤 var {tyep,name} // 語法錯誤 let {type,name} // 語法錯誤 const {type,name} 使用解構給已經宣告的變數賦值,如下 let node = { type:&qu…

    個人 2025年3月8日
    1.2K00
  • TS珠峰 004【學習筆記】

    類型體操 type-1 // 內建 // Partial Required Readonly 修飾類型的 // Pick Omit 處理資料結構 // Exclude Extract 處理集合類型的 // Parameters ReturnType infer // 字串類型,樣板字串`${}` + infer PartialPropsOptional …

    個人 2025年3月27日
    1.4K00
  • 深入理解ES6 003【學習筆記】

    函數參數預設值,以及一些關於 arguments 物件,如何使用運算式作為參數、參數的暫時性死區。 以前設定預設值總是利用在含有邏輯或運算子的運算式中,前一個值是 false 時,總是回傳後面那個值,但如果我們給參數傳入 0 時,就會有些麻煩。 需要去驗證一下型別 function makeRequest(url,timeout,callback){ timeout = t…

    個人 2025年3月8日
    1.2K00
  • Go 工程師體系課 010【學習筆記】

    安裝 Elasticsearch (可理解為資料庫) 和 Kibana (可理解為連接工具)。Elasticsearch 和 Kibana (5601) 的版本必須保持一致。MySQL 對照學習 Elasticsearch (ES) 術語對照:MySQL 的 database 對應 Elasticsearch 的 index (索引);MySQL 的 table 對應 Elasticsearch 的 type (從 7.x 版起固定為 _doc,8.x 版徹底移除多個類型…

    個人 2025年11月25日
    32400
  • Go 工程師體系課 014【學習筆記】

    rocketmq 快速入門 請參考我們的各種配置(podman)以瞭解安裝方式。概念介紹 RocketMQ 是阿里巴巴開源、Apache 頂級專案的分散式訊息中介軟體,核心元件: NameServer:服務發現與路由 Broker:訊息儲存、遞送、拉取 Producer:訊息生產者(傳送訊息) Consumer:訊息消費者(訂閱並消費訊息) Topic/Tag:主題/…

    個人 2025年11月25日
    16700
歡迎🌹 Coding never stops, keep learning! 💡💻 光臨🌹