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://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 處理集合類型的 // Paramters RetrunValue 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【學習筆記】

    es 安裝 elasticsearch(理解為庫) kibana(理解為連接工具)es 和 kibana(5601) 的版本要保持一致 MySQL 對照學習 Elasticsearch(ES) 術語對照 MySQL Elasticsearch database index(索引) table type(7.x 起固定為 _doc,8.x 徹底移除多 type…

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

    rocketmq 快速入門 去我們的各種配置(podman)看是怎麼安裝的 概念介紹 RocketMQ 是阿里開源、Apache 頂級項目的分布式消息中間件,核心組件: NameServer:服務發現與路由 Broker:消息存儲、投遞、拉取 Producer:消息生產者(發送消息) Consumer:消息消費者(訂閱並消費消息) Topic/Tag:主題/…

    個人 2025年11月25日
    16400
簡體中文 繁體中文 English
歡迎🌹 Coding never stops, keep learning! 💡💻 光臨🌹