Go工程師體系課 006

8次閱讀

項目結構說明:user-web 模塊

user-webjoyshop_api 工程中的用戶服務 Web 層模塊,負責處理用戶相關的 HTTP 請求、參數校驗、業務路由以及調用後端接口等功能。以下是目錄結構說明:

user-web/
├── api/           # 控制器層,定義業務接口處理邏輯
├── config/        # 配置模塊,包含系統配置結構體及讀取邏輯
├── forms/         # 請求參數結構體與校驗邏輯,主要用於表單解析與綁定
├── global/        # 全局對象,如數據庫連接、配置、日誌等全局變量定義
├── initialize/    # 系統初始化模塊,如數據庫、路由、配置加載等初始化邏輯
├── middlewares/   # 中間件定義,如鑑權、日誌記錄、跨域處理等
├── proto/         # gRPC 生成的 protobuf 文件,用於與後端服務通信
├── router/        # 路由註冊模塊,將 API 綁定到具體路徑
├── utils/         # 工具函數模塊,包含通用方法,如分頁、加解密、轉換等
├── validator/     # 自定義參數驗證器,用於配合表單驗證規則
├── main.go        # 啓動入口,負責加載配置、初始化組件並啓動服務

快速開始

# 編譯並運行 user-web 服務
go run user-web/main.go

注意事項

  • 配置文件路徑和格式請在 initialize/config.go 中查看。
  • 路由入口位於 router/router.go,可從此處瞭解 API 分組與綁定。
  • 若使用了 gRPC,請確保 proto 文件生成後已正確引用。

Go 日誌庫 zap 使用說明

zap 是 Uber 開源的高性能結構化日誌庫,廣泛應用於 Go 項目中。


📦 安裝

go get -u go.uber.org/zap

🚀 基本使用

package main

import ("go.uber.org/zap")

func main() {logger, _ := zap.NewProduction()
    defer logger.Sync() // 確保緩存日誌寫入文件

    logger.Info(" 這是一個 Info 日誌 ",
        zap.String("url", "http://example.com"),
        zap.Int("attempt", 3),
        zap.Duration("backoff", 200),
    )
}

🛠️ 自定義日誌配置

config := zap.NewDevelopmentConfig()
config.OutputPaths = []string{"stdout", "./log/zap.log"}

logger, err := config.Build()
if err != nil {panic(err)
}
defer logger.Sync()

logger.Debug(" 自定義配置日誌 ")

🧩 常用字段類型

  • zap.String(key, val string)
  • zap.Int(key string, val int)
  • zap.Bool(key string, val bool)
  • zap.Time(key string, val time.Time)
  • zap.Any(key string, val interface{})

📚 更多文檔

官方文檔:https://pkg.go.dev/go.uber.org/zap
GitHub 倉庫:https://github.com/uber-go/zap

package main

import (
 "time"
 "go.uber.org/zap"
)

// 自定義生產環境 Logger 配置
func NewLogger() (*zap.Logger, error) {cfg := zap.NewProductionConfig()
 cfg.OutputPaths = []string{"./myproject.log", // 輸出日誌到當前目錄下的 myproject.log 文件}
 return cfg.Build()}

func main() {
 // 初始化 logger
 logger, err := NewLogger()
 if err != nil {panic(err)
 }
 defer logger.Sync()

 // 獲取 SugarLogger(提供更簡潔的格式化輸出)su := logger.Sugar()
 defer su.Sync()

 url := "https://imooc.com"
 su.Info("failed to fetch URL",
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
 )
}

Go 的配置文件管理 – Viper

1. 介紹

Viper 是適用於 Go 應用程序的完整配置解決方案。它被設計用於在應用程序中工作,並且可以處理所有類型的配置需求和格式。它支持以下特性:

  • 設置默認值
  • JSON, TOML, YAML, HCL, .envfileJava properties 格式的配置文件讀取配置信息
  • 實時監控和重新讀取配置文件(可選)
  • 從環境變量中讀取
  • 從遠程配置系統(如 etcd 或 Consul)讀取並監控配置變化
  • 從命令行參數讀取配置
  • 從 buffer 讀取配置
  • 顯式配置值

2. YAML 教程

教程地址:[暫無提供]

3. 安裝

go get github.com/spf13/viper

GitHub 地址:spf13/viper

4. 使用示例

package main

import (
    "fmt"
    "github.com/spf13/viper"
)

func main() {
    // 設置配置文件名和類型
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".") // 配置文件路徑

    // 讀取配置
    if err := viper.ReadInConfig(); err != nil {panic(fmt.Errorf("fatal error config file: %w", err))
    }

    // 訪問配置值
    port := viper.GetInt("server.port")
    fmt.Printf("Server Port: %d\n", port)
}
package main

import "github.com/spf13/viper"

type ServerConfig struct {
 ServiceName string `mapstructure:"name"`
 Port        int    `mapstructure:"port"`
}

func main() {v := viper.New()
 v.SetConfigName("config")
 v.SetConfigType("yaml")
 v.AddConfigPath("./viper_test/ch01")
 err := v.ReadInConfig()
 if err != nil {panic(err)
 }

 // Get the value of the "name" key
 name := v.GetString("name")
 println(name)

 // Get the value of the "age" key
 age := v.GetInt("age")
 println(age)

 sCfig := &ServerConfig{}
 if err := v.Unmarshal(sCfig); err != nil {panic(err)
 }
 println(sCfig.ServiceName)
 println(sCfig.Port)
}
package main

import (
 "fmt"
 "github.com/spf13/viper"
)

type MysqlConfig struct {
 Host string `mapstructure:"host"`
 Port int    `mapstructure:"port"`
}
type ServerConfig struct {
 ServiceName string      `mapstructure:"name"`
 MysqlInfo   MysqlConfig `mapstructure:"mysql"`
}

func main() {v := viper.New()
 v.SetConfigName("config")
 v.SetConfigType("yaml")
 v.AddConfigPath("./viper_test/ch02")
 err := v.ReadInConfig()
 if err != nil {panic(err)
 }

 sCfig := &ServerConfig{}
 if err := v.Unmarshal(sCfig); err != nil {panic(err)
 }
 fmt.Println(sCfig)
}

不用改任何代碼,將線下開發和線上生產的配置文件,環境區分開,還可以動態監控配置變化 v.WatchConfig(), 然後通過 v.OnConfigChange(func(e fsnotify.Event){fmt.Println("config file change",e.Name)})

什麼是 JWT?

JWT(JSON Web Token)是一種用於在網絡應用環境間安全傳輸信息的開放標準(RFC 7519)。JWT 是由三部分組成的字符串,分別是:

  1. Header(頭部)
  2. Payload(負載)
  3. Signature(簽名)

結構示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

使用場景

  • 前後端分離登錄驗證
  • 用戶身份認證
  • 權限控制

JWT 登錄驗證流程

1. 用戶登錄

  • 用戶提交用戶名和密碼到後端。

2. 服務器驗證用戶信息

  • 驗證成功後,生成一個 JWT,返回給前端。
  • JWT 通常包含用戶 ID、過期時間等信息。

3. 前端存儲 Token

  • 一般存儲在 localStorage 或 sessionStorage,也可以存在 Cookie 中。

4. 發送請求時攜帶 Token

  • 前端發送後續請求時,將 JWT 放在 HTTP 請求頭中,例如:

Authorization: Bearer <your_token>

5. 後端驗證 Token

  • 後端中間件提取並驗證 JWT。
  • 驗證通過則繼續處理請求,否則返回 401 未授權。

使用示例(Node.js + Express + jsonwebtoken)

安裝依賴

npm install express jsonwebtoken body-parser

登錄接口(生成 Token)

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const SECRET_KEY = 'your_secret_key';

app.post('/login', (req, res) => {const { username, password} = req.body;
  if (username === 'admin' && password === '123456') {const token = jwt.sign({ username}, SECRET_KEY, {expiresIn: '1h'});
    res.json({token});
  } else {res.status(401).json({message: '登錄失敗'});
  }
});

驗證中間件

function authMiddleware(req, res, next) {const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.sendStatus(401);

  jwt.verify(token, SECRET_KEY, (err, user) => {if (err) return res.sendStatus(403);
    req.user = user;
    next();});
}

受保護接口

app.get('/protected', authMiddleware, (req, res) => {res.json({ message: '訪問成功', user: req.user});
});

啓動服務器

app.listen(3000, () => {console.log('Server running on http://localhost:3000');
});

注意事項

  • 不要將敏感信息放入 JWT 的 Payload 中。
  • 定期更新密鑰(SECRET_KEY)以增強安全性。
  • 控制 Token 的過期時間,避免長期有效帶來的風險。

圖形驗證碼

mojotv.cn/go/refactor-base64-captcha

基於配置文件微服務的解決方案 (註冊中心)

什麼是服務註冊和發現

假設這個產品已經在線上運行,有一天運營想搞一場促銷活動,那麼我們相對應的【用戶服務】可能就要新開出幾個微服務實例來支撐這場促銷活動。而與此同時,作爲高牆程序員的你就只有手動去 API gateway 中添加新增加的這個微服務實例的 ip 與 port,一個真正在線的微服務系統可能有成百上千個微服務,難道也要一個一個去手動添加?有沒有讓系統能自動去操作的方式呢?答案當然是有的。

當我們添加一個微服務實例的時候,微服務就會將自己的 ip 與 port 發送到註冊中心,在註冊中心裏面記錄起來。當 API gateway 需要訪問這些微服務的時候,就會去註冊中心找到相應的 ip 與 port,從而實現自動化操作。

技術選型

Consul 與其他常見服務發現框架對比:

名稱 優點 缺點 接口 一致性算法
zookeeper 1. 功能強大,不僅僅只是服務發現
2. 提供 watcher 機制能實時獲取服務提供者的狀態
3. dubbo 等框架支持
1. 沒有健康檢查
2. 需在服務中集成 sdk,複雜度高
3. 不支持多數據中心
sdk Paxos
consul 1. 簡單易用,不需要集成 sdk
2. 自帶健康檢查
3. 支持多數據中心
4. 提供 web 管理界面
1. 不能實時獲取服務信息的變化通知 http/dns Raft
etcd 1. 簡單易用,不需要集成 sdk
2. 可配置性強
1. 沒有健康檢查
2. 需配合第三方工具一起完成服務發現
3. 不支持多數據中心
http Raft

使用 Docker Compose 部署 Consul(最新穩定版)

一、前置準備

建議在項目目錄中準備如下結構,用於持久化 Consul 數據並支持配置掛載:

.
├── docker-compose.yaml
└── consul
    ├── config         # 放置 JSON 或 HCL 配置文件
    └── data           # Consul 數據將持久化到這裏

二、docker-compose.yaml 配置內容

version: '3.8' # 說明:此字段在 Compose V2 中不是必須的,但保留並不會影響使用

services:
  consul:
    image: hashicorp/consul:latest
    container_name: consul
    restart: unless-stopped
    ports:
      - '8500:8500' # Web UI 和 HTTP API
      - '8600:8600/udp' # DNS(UDP)- '8600:8600' # DNS(TCP)volumes:
      - ./consul/data:/consul/data
      - ./consul/config:/consul/config
    command: agent -server -bootstrap -ui -client=0.0.0.0 -data-dir=/consul/data -config-dir=/consul/config

三、啓動 Consul

在當前目錄下運行以下命令啓動容器:

docker-compose up -d

啓動完成後,Consul Web UI 可通過以下地址訪問:

http://localhost:8500

四、說明

  • version: '3.8':Compose V2 中此字段已非必需,可省略。
  • -client=0.0.0.0:允許外部主機訪問 Consul 服務。
  • -bootstrap:啓用單節點引導模式,適用於開發或測試環境。

如需生產部署,請使用 -bootstrap-expect=N 配置集羣節點數量,並關閉 bootstrap。

dns 服務得能用 我們使用 dig 來測試

dig @127.0.0.1 -p 8600 consul.service.consul SRv

; <<>> DiG 9.10.6 <<>> @127.0.0.1 -p 8600 consul.service.consul SRv
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19421
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 4
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;consul.service.consul.  IN SRV

;; ANSWER SECTION:
consul.service.consul. 0 IN SRV 1 1 8300 d3fd490264e2.node.dc1.consul.

;; ADDITIONAL SECTION:
d3fd490264e2.node.dc1.consul. 0 IN A 172.21.0.2
d3fd490264e2.node.dc1.consul. 0 IN TXT "consul-network-segment="
d3fd490264e2.node.dc1.consul. 0 IN TXT "consul-version=1.21.0"

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Thu May 08 11:52:56 +07 2025
;; MSG SIZE  rcvd: 184
  1. 添加服務
    https://www.consul.io/api-docs/agent/service#register-service

  2. 刪除服務
    https://www.consul.io/api-docs/agent/service#deregister-service

  3. 設置健康檢查
    https://www.consul.io/api-docs/agent/check

  4. 同一個服務註冊多個實例
    (可在註冊服務時使用不同的 ID)

  5. 獲取服務
    https://www.consul.io/api-docs/agent/check#list-checks

package main

import (
 "fmt"
 "github.com/hashicorp/consul/api"
)

func main() {
 // 1. 創建一個新的 Consul 客戶端
 //_ = Register("192.168.1.7", 8022, "user-web", []string{"joyshop", "test", "walker"}, "user-web")
 //AllService()
 FilterService()}

func Register(address string, port int, name string, tags []string, id string) error {cfg := api.DefaultConfig()
 cfg.Address = "192.168.1.7:8500"
 client, err := api.NewClient(cfg)
 if err != nil {panic(err)
 }
 registration := new(api.AgentServiceRegistration)
 registration.ID = id
 registration.Name = name
 registration.Address = address
 registration.Port = port
 registration.Tags = tags
 // 生成對應的檢查對象
 check := new(api.AgentServiceCheck)
 check.HTTP = fmt.Sprintf("http://%s:%d/health", address, port)
 check.Interval = "5s"
 check.Timeout = "5s"
 check.DeregisterCriticalServiceAfter = "10s"
 registration.Check = check
 err = client.Agent().ServiceRegister(registration)
 if err != nil {panic(err)
 }
 return nil
}

func AllService() {cfg := api.DefaultConfig()
 cfg.Address = "192.168.1.7:8500"
 client, err := api.NewClient(cfg)
 if err != nil {panic(err)
 }
 services, err := client.Agent().Services()
 if err != nil {panic(err)
 }
 for _, service := range services {fmt.Println(service.Service)
 }

}

func FilterService() {cfg := api.DefaultConfig()
 cfg.Address = "192.168.1.7:8500"
 client, err := api.NewClient(cfg)
 if err != nil {panic(err)
 }
 services, err := client.Agent().ServicesWithFilter(`Service == "user-web"`)

 if err != nil {panic(err)
 }
 for _, service := range services {fmt.Println(service.Service)
 }
}

動態獲取可用端口

grpc-consul-resolver

/*
 * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
 * @Date: 2025-05-10 13:47:24
 * @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
 * @LastEditTime: 2025-05-10 13:59:13
 * @FilePath: /GormStart/grpclb/main.go
 * @Description: 這是默認設置, 請設置 `customMade`, 打開 koroFileHeader 查看配置 進行設置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
package main

import (
 "GormStart/grpclb/proto"
 "context"
 "log"

 _ "github.com/mbobakov/grpc-consul-resolver" // It's important

 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials/insecure"
)

func main() {
 conn, err := grpc.NewClient(
  "consul://192.168.1.7:8500/user-srv?wait=14s&tag=joyshop",
  grpc.WithTransportCredentials(insecure.NewCredentials()),
  grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
 )
 if err != nil {log.Fatal(err)
 }
 defer conn.Close()

 userSrvClient := proto.NewUserClient(conn)
 rsp, err := userSrvClient.GetUserList(context.Background(), &proto.PageInfo{
  Page:     1,
  PageSize: 2,
 })
 if err != nil {log.Fatal(err)
 }
 for index, data := range rsp.Data {log.Printf(" 第 %d 條數據: %v", index, data)
 }
}

分佈式配置中心

1. 爲什麼需要分佈式配置中心

我們現在有一個項目,使用 gin 進行開發的,配置文件的話我們知道是一個叫做 config.yaml 的文件。
我們也知道這個配置文件會在項目啓動的時候被加載到內存中進行使用的。


考慮兩種情況

a. 添加配置項

i. 你現在的用戶服務有 10 個部署實例,那麼添加配置項你得去十個地方修改配置文件還得重新啓動等。
ii. 即使 Go 的 viper 能完成修改配置文件自動生效,那麼你得考慮其他語言是否也能做到這點,其他的服務是否也一定會使用 viper?


b. 修改配置項

大量的服務可能會使用同一個配置,比如我要更改 jwt 的 secret,這麼多實例怎麼辦?


c. 開發、測試、生產環境如何隔離

前面雖然已經介紹了 viper,但是依然一樣的問題,這麼多服務如何統一?這種考慮因素?

nacos

version: '3.8'

services:
  nacos:
    image: nacos/nacos-server:v2.3.2
    container_name: nacos-standalone
    ports:
      - '8848:8848' # Web UI & API
      - '9848:9848' # gRPC 通信端口(2.x 版本啓用)- '9849:9849' # gRPC 通信端口
    environment:
      MODE: standalone
      NACOS_AUTH_ENABLE: 'false'
      JVM_XMS: 256m
      JVM_XMX: 512m
      JVM_XMN: 128m
    volumes:
      - ./nacos-data:/home/nacos/data
    restart: unless-stopped

地址

命名空間: 可以隔離配置集,將某些配置放到某一個命名空間之下,命名空間是用來區分微服務的
Group: 區分環境(dev test prod)
dataId: 可以理解就是一個配置文件

go 語言獲取配置信息(能獲取配置, 能監聽修改,)

正文完
 0