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
上一篇 2026年3月7日 14:00
下一篇 2026年3月7日 12:00

相關推薦

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

    區塊作用域繫結 之前的變數宣告 `var` 無論在哪裡宣告,都會被視為作用域頂部宣告。由於函式是一等公民,因此順序通常是 `function 函式名稱()`、`var 變數`。 區塊宣告 區塊宣告用於宣告在指定區塊的作用域之外無法存取的變數。區塊作用域存在於: 函式內部 區塊中(字元 `{` 和 `}` 之間的區域) 暫時性死區 JavaScript 引擎在掃描程式碼發現變數宣告時,要麼會將它們提升至作…

    個人 2025年3月8日
    1.9K00
  • 深入理解ES6 010【學習筆記】

    改進的陣列功能 new Array() 的怪異行為,當建構函式傳入一個數值型的值,那麼陣列的 length 屬性會被設為該值;如果傳入多個值,此時無論這些值是不是數值型的,都會變為陣列的元素。這個特性令人困惑,你不可能總是注意傳入資料的類型,所以存在一定的風險。 Array.of() 無論傳多少個參數,不存在單一數值的特例(一個參數且數值型),總是返回包含所有參數…

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

    商品微服務 實體結構說明 本模組包含以下核心實體: 商品(Goods) 商品分類(Category) 品牌(Brands) 輪播圖(Banner) 品牌分類(GoodsCategoryBrand) 1. 商品(Goods) 描述平台中實際展示和銷售的商品資訊。 欄位說明 欄位名 類型 說明 name String 商品名稱,必填 brand Pointer …

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

    Protocol Buffers 入門指南 1. 簡介 Protocol Buffers(簡稱 protobuf)是 Google 開發的一種語言無關、平台無關、可擴充的結構化資料序列化機制。與 JSON、XML 等序列化方式相比,protobuf 更小、更快、更簡單。 專案首頁:https://github.com/protocolbuffers/prot…

    個人 2025年11月25日
    1.3K00
  • Nuxt3_掃盲 入門與原理介紹【學習筆記】

    Nuxt 3 入門與原理介紹 💡 什麼是 Nuxt 3? Nuxt 3 是基於 Vue 3 和 Vite 打造的全端前端框架,支援: 伺服器端渲染(SSR) 靜態網站生成(SSG) 單頁應用程式(SPA) 建構全端應用程式(支援 API) Nuxt 3 是 Vue 的「加強版」,幫你簡化專案結構與開發流程。 🔧 核心原理 功能 Nuxt 如何處理 ✅ 頁面路由 自動根…

    個人 2025年4月6日
    2.2K00