← 返回
个人 2025.11.24

Go工程师体系课 006【学习笔记】

个人

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 与其他常见服务发现框架对比:

名称优点缺点接口一致性算法
zookeeper1. 功能强大,不仅仅只是服务发现
2. 提供 watcher 机制能实时获取服务提供者的状态
3. dubbo 等框架支持1. 没有健康检查
2. 需在服务中集成 sdk,复杂度高
3. 不支持多数据中心sdkPaxos
consul1. 简单易用,不需要集成 sdk
2. 自带健康检查
3. 支持多数据中心
4. 提供 web 管理界面1. 不能实时获取服务信息的变化通知http/dnsRaft
etcd1. 简单易用,不需要集成 sdk
2. 可配置性强1. 没有健康检查
2. 需配合第三方工具一起完成服务发现
3. 不支持多数据中心httpRaft

使用 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 语言获取配置信息(能获取配置,能监听修改,)