Go工程师体系课 006

项目结构说明: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 语言获取配置信息(能获取配置,能监听修改,)

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6772

(0)
Walker的头像Walker
上一篇 13小时前
下一篇 2025年11月25日 02:00

相关推荐

  • 编程基础 0008_标准库进阶

    Go 标准库进阶 系统整理 Go 标准库中最常用的包,重点覆盖 io、os、bufio、strings、time、fmt 等 1. io 包核心接口 Go 的 I/O 设计围绕几个核心接口展开,几乎所有 I/O 操作都基于它们。 // 最基础的两个接口 type Reader interface { Read(p []byte) (n int, err er…

    后端开发 19小时前
    500
  • 编程基础 0010_Go底层原理与源码精华

    Go 底层原理与源码精华 基于《Go 源码剖析》(雨痕, 第五版下册)、《Go 1.4 runtime》、《Go 学习笔记 第四版》、《Golang 性能优化》、《Go Execution Modes》等资料整理,并补充现代 Go 版本的变化。 一、Go 编译器与链接器 1.1 编译流程概览 Go 的编译过程分为以下阶段: 源码 (.go) --> 词…

    后端开发 20小时前
    500
  • Go工程师体系课 014

    rocketmq 快速入门 去我们的各种配置(podman)看是怎么安装的 概念介绍 RocketMQ 是阿里开源、Apache 顶级项目的分布式消息中间件,核心组件: NameServer:服务发现与路由 Broker:消息存储、投递、拉取 Producer:消息生产者(发送消息) Consumer:消息消费者(订阅并消费消息) Topic/Tag:主题/…

    后端开发 2小时前
    100
  • 编程基础 0011_Go并发与分布式实战精华

    Go 并发与分布式实战精华 参考:《Go 并发编程实战》(郝林)、《Mastering Concurrency in Go》(Nathan Kozyra)、《Go 语言构建高并发分布式系统实践》 1. 并发原语深入 1.1 atomic 包 atomic 操作直接映射到 CPU 指令(如 LOCK CMPXCHG),比 mutex 快一个数量级。 impor…

    后端开发 21小时前
    100
  • 编程基础 0003_Web_beego开发

    Web 开发之 Beego 使用 go get 安装 bee 工具与 beego Bee Beego 使用 bee 工具初始化 Beego 项目 在$GOPATH/src 目录下执行 bee create myapp 使用 bee 工具热编译 Beego 项目 在$GOPATH/src/myapp 目录下执行 bee start myapp // hello…

    后端开发 17小时前
    500
简体中文 繁体中文 English