目录
[1.1 go-zero基础](#1.1 go-zero基础)
[1.1.1 go-zero](#1.1.1 go-zero)
[1.1.2 工程级代码框架](#1.1.2 工程级代码框架)
[1.2.3 go-zero 高级特性:](#1.2.3 go-zero 高级特性:)
[1.2 go-zero在WEB3.0中的应用场景](#1.2 go-zero在WEB3.0中的应用场景)
[1.2.1 数据索引与缓存服务(The Graph 类替代方案)](#1.2.1 数据索引与缓存服务(The Graph 类替代方案))
[1.2.2 去中心化应用的 API 网关](#1.2.2 去中心化应用的 API 网关)
[1.2.3 与智能合约的高效交互层](#1.2.3 与智能合约的高效交互层)
[1.2.4 数字货币交易平台的核心后端](#1.2.4 数字货币交易平台的核心后端)
[1.2.5 跨链桥(Bridge)的后端服务](#1.2.5 跨链桥(Bridge)的后端服务)
[1.2.6 Web3.0 社交平台的后端](#1.2.6 Web3.0 社交平台的后端)
[1.3 go-zero 和 Gin 对比](#1.3 go-zero 和 Gin 对比)
[1.4 Go-zero特点](#1.4 Go-zero特点)
[2.1 go-zero安装](#2.1 go-zero安装)
[2.1.1 安装 goctl](#2.1.1 安装 goctl)
[2.1.2 安装 protoc](#2.1.2 安装 protoc)
[2.1.3 配置 VS Code 插件](#2.1.3 配置 VS Code 插件)
[2.2 创建初始化项目](#2.2 创建初始化项目)
[2.2.1 创建项目](#2.2.1 创建项目)
[2.2.2 初始化项目](#2.2.2 初始化项目)
[2.2.3 目录介绍](#2.2.3 目录介绍)
[2.3 修改参数运行项目](#2.3 修改参数运行项目)
[2.3.1 编写业务逻辑 运行项目](#2.3.1 编写业务逻辑 运行项目)
[2.3.2 goctl api 常用命令详解](#2.3.2 goctl api 常用命令详解)
[2.4 热加载简介](#2.4 热加载简介)
[第三章:api 概述](#第三章:api 概述)
[3.1 请求参数](#3.1 请求参数)
[3.1.1 参数介绍](#3.1.1 参数介绍)
[3.1.2 可空参数](#3.1.2 可空参数)
[3.1.3 参数默认值与参数枚举值](#3.1.3 参数默认值与参数枚举值)
[3.2 编写一个获取轮播图列表的Api服务](#3.2 编写一个获取轮播图列表的Api服务)
[3.2.1 新建shop.api](#3.2.1 新建shop.api)
[3.2.2 生成项目](#3.2.2 生成项目)
[3.2.3 下载依赖](#3.2.3 下载依赖)
[3.2.4 修改业务代码](#3.2.4 修改业务代码)
[3.2.5 访问项目](#3.2.5 访问项目)
[3.3 在上面的项目中加入获取文章列表的Api服务(GET传值、动态路由)](#3.3 在上面的项目中加入获取文章列表的Api服务(GET传值、动态路由))
[3.3.1 修改shop.api](#3.3.1 修改shop.api)
[3.3.2 生成项目](#3.3.2 生成项目)
[3.3.3 修改业务代码](#3.3.3 修改业务代码)
[3.3.4 访问项目](#3.3.4 访问项目)
[3.4 @server 配置前缀以及对项目进行分组](#3.4 @server 配置前缀以及对项目进行分组)
[3.4.1 修改shop.api](#3.4.1 修改shop.api)
[3.4.2 生成项目](#3.4.2 生成项目)
[3.4.3 修改业务代码](#3.4.3 修改业务代码)
[3.4.4 访问项目](#3.4.4 访问项目)
[3.5 编写带有中间件的 api 服务](#3.5 编写带有中间件的 api 服务)
[3.5.1 修改shop.api](#3.5.1 修改shop.api)
[3.5.2 生成项目](#3.5.2 生成项目)
[3.5.3 完善中间件代码](#3.5.3 完善中间件代码)
[3.5.4 路由中注册中间件](#3.5.4 路由中注册中间件)
[3.6 编写带有超时配置的 api 服务](#3.6 编写带有超时配置的 api 服务)
[3.6.1 配置](#3.6.1 配置)
[3.6.1 @doc 语句](#3.6.1 @doc 语句)
[3.7 import 语句](#3.7 import 语句)
[3.7.1 基本语法](#3.7.1 基本语法)
[3.7.2 使用](#3.7.2 使用)
[第四章:go-zero 原生数据库访问方案](#第四章:go-zero 原生数据库访问方案)
[4.1 go-zero 中绑定 PostgreSQL 数据库](#4.1 go-zero 中绑定 PostgreSQL 数据库)
[4.1.1 项目创建](#4.1.1 项目创建)
[4.1.2 集成 PostgreSQL 数据库](#4.1.2 集成 PostgreSQL 数据库)
[4.2 查询多条数据](#4.2 查询多条数据)
[4.2.1 修改 Model 层](#4.2.1 修改 Model 层)
[4.2.2 修改业务逻辑层](#4.2.2 修改业务逻辑层)
[4.3 增加数据](#4.3 增加数据)
[4.3.1 修改shop.api](#4.3.1 修改shop.api)
[4.3.2 生成项目](#4.3.2 生成项目)
[4.3.3 修改业务逻辑](#4.3.3 修改业务逻辑)
[4.4 修改数据](#4.4 修改数据)
[4.4.1 修改shop.api](#4.4.1 修改shop.api)
[4.4.2 生成项目](#4.4.2 生成项目)
[4.4.3 修改业务逻辑](#4.4.3 修改业务逻辑)
[4.5 删除数据](#4.5 删除数据)
[4.5.1 修改业务逻辑](#4.5.1 修改业务逻辑)
[第五章:go-zero 中原生 SQL 支持](#第五章:go-zero 中原生 SQL 支持)
[5.1 引言](#5.1 引言)
[5.2 为什么需要原生 SQL?](#5.2 为什么需要原生 SQL?)
[5.3 执行原生 SQL 的环境准备](#5.3 执行原生 SQL 的环境准备)
[5.3.1 创建数据库连接](#5.3.1 创建数据库连接)
[5.3.2 配置数据库驱动](#5.3.2 配置数据库驱动)
[5.4 原生 SQL 的基本用法](#5.4 原生 SQL 的基本用法)
[5.4.1 执行 DML(插入、更新、删除)](#5.4.1 执行 DML(插入、更新、删除))
[5.4.2 查询单行数据](#5.4.2 查询单行数据)
[5.4.3 查询多行数据](#5.4.3 查询多行数据)
[3.4 高级:使用 MapScan 灵活处理结果](#3.4 高级:使用 MapScan 灵活处理结果)
[5.5 事务处理](#5.5 事务处理)
[5.5.1 基本事务用法](#5.5.1 基本事务用法)
[5.5.2 手动控制事务](#5.5.2 手动控制事务)
[5.6 高级特性](#5.6 高级特性)
[5.6.1 预处理语句(Prepared Statement)](#5.6.1 预处理语句(Prepared Statement))
[5.6.2 命名参数](#5.6.2 命名参数)
[5.6.3 动态 SQL 构建](#5.6.3 动态 SQL 构建)
[5.7 安全与性能注意事项](#5.7 安全与性能注意事项)
[5.7.1 防止 SQL 注入](#5.7.1 防止 SQL 注入)
[5.7.2 连接池配置](#5.7.2 连接池配置)
[5.7.3 日志与监控](#5.7.3 日志与监控)
[5.8 实战案例:Web3.0 NFT 市场后台](#5.8 实战案例:Web3.0 NFT 市场后台)
[5.8.1 场景描述](#5.8.1 场景描述)
[5.8.2 实现步骤](#5.8.2 实现步骤)
[5.8.3 性能优化建议](#5.8.3 性能优化建议)
[5.9 与 GORM 的比较及选择](#5.9 与 GORM 的比较及选择)
[第六章:go-zero 中 JWT 集成](#第六章:go-zero 中 JWT 集成)
[6.1 JWT 在企业应用中的定位](#6.1 JWT 在企业应用中的定位)
[6.2 项目结构设计(推荐)](#6.2 项目结构设计(推荐))
[6.3 API 定义](#6.3 API 定义)
[6.3.1 登录接口 api/user.api](#6.3.1 登录接口 api/user.api)
[6.3.2 受保护接口 api/address.api](#6.3.2 受保护接口 api/address.api)
[6.3.3 主入口 jwt.api](#6.3.3 主入口 jwt.api)
[6.4 代码生成与依赖管理](#6.4 代码生成与依赖管理)
[6.5 统一业务处理封装(internal/biz)](#6.5 统一业务处理封装(internal/biz))
[6.5.1 业务状态码 code.go](#6.5.1 业务状态码 code.go)
[6.5.2 自定义错误类型 error.go](#6.5.2 自定义错误类型 error.go)
[6.5.3 统一响应 response.go](#6.5.3 统一响应 response.go)
[6.6 全局错误处理(main.go)](#6.6 全局错误处理(main.go))
[6.7 JWT 集成](#6.7 JWT 集成)
[6.7.1 配置文件 etc/user-api.yaml](#6.7.1 配置文件 etc/user-api.yaml)
[6.7.2 配置结构体 internal/config/config.go](#6.7.2 配置结构体 internal/config/config.go)
[6.7.3 JWT 生成工具 internal/biz/jwt.go](#6.7.3 JWT 生成工具 internal/biz/jwt.go)
[6.7.4 登录逻辑 internal/logic/account/loginlogic.go](#6.7.4 登录逻辑 internal/logic/account/loginlogic.go)
[6.7.5 受保护路由的 JWT 中间件 internal/handler/routes.go](#6.7.5 受保护路由的 JWT 中间件 internal/handler/routes.go)
[6.7.6 在业务逻辑中获取用户信息](#6.7.6 在业务逻辑中获取用户信息)
[6.8 企业级最佳实践](#6.8 企业级最佳实践)
[第七章:go-zero 跨域配置(企业级实践)](#第七章:go-zero 跨域配置(企业级实践))
[7.1 跨域问题根源](#7.1 跨域问题根源)
[7.2 go-zero 中 CORS 配置方式](#7.2 go-zero 中 CORS 配置方式)
[7.2.1 允许所有来源(仅开发环境)](#7.2.1 允许所有来源(仅开发环境))
[7.2.2 允许指定域名(推荐生产环境)](#7.2.2 允许指定域名(推荐生产环境))
[7.2.3 自定义允许的请求头(如需携带自定义 header)](#7.2.3 自定义允许的请求头(如需携带自定义 header))
[7.2.4 同时指定域名与请求头](#7.2.4 同时指定域名与请求头)
[7.2.5 完全自定义 CORS 策略(适用于复杂需求)](#7.2.5 完全自定义 CORS 策略(适用于复杂需求))
[7.3 企业级 CORS 最佳实践](#7.3 企业级 CORS 最佳实践)
[7.4 示例:支持多域名与凭证](#7.4 示例:支持多域名与凭证)
[8.1 服务注册与发现](#8.1 服务注册与发现)
[8.1.1 为什么 Web3.0 项目需要它?](#8.1.1 为什么 Web3.0 项目需要它?)
[8.1.2 实战:集成 etcd](#8.1.2 实战:集成 etcd)
[8.2 限流、熔断与降载](#8.2 限流、熔断与降载)
[8.2.1 限流配置](#8.2.1 限流配置)
[8.2.2 熔断与降载](#8.2.2 熔断与降载)
[8.3 分布式链路追踪](#8.3 分布式链路追踪)
[8.3.1 集成 Jaeger](#8.3.1 集成 Jaeger)
[9.1 多环境配置](#9.1 多环境配置)
[9.2 配置中心(Nacos / Apollo)](#9.2 配置中心(Nacos / Apollo))
[10.1 事务处理](#10.1 事务处理)
[10.2 分库分表与读写分离](#10.2 分库分表与读写分离)
[10.3 缓存穿透/击穿/雪崩防范](#10.3 缓存穿透/击穿/雪崩防范)
[11.1 容器化](#11.1 容器化)
[11.2 Kubernetes 部署](#11.2 Kubernetes 部署)
[11.3 优雅退出](#11.3 优雅退出)
[11.4 日志规范](#11.4 日志规范)
[12.1 单元测试](#12.1 单元测试)
[12.2 集成测试](#12.2 集成测试)
[12.3 压测与性能调优](#12.3 压测与性能调优)
[13.1 敏感信息保护](#13.1 敏感信息保护)
[13.2 防 SQL 注入](#13.2 防 SQL 注入)
[13.3 API 防重放攻击](#13.3 API 防重放攻击)
[13.4 限流高级配置](#13.4 限流高级配置)
[15.1 集成区块链节点](#15.1 集成区块链节点)
[15.2 集成 IPFS](#15.2 集成 IPFS)
[16.1 定制 goctl 模板](#16.1 定制 goctl 模板)
[16.2 API 文档自动化](#16.2 API 文档自动化)
第一章:go-zero概述
1.1 go-zero基础
1.1.1 go-zero
go-zero 是一个集成了多种工程实践的现代化 Web 和 RPC 框架,它既是一个高性能的 Web 框架,也是一个功能完备的微服务框架。其核心设计理念是以服务稳定为优先 ,通过内置丰富的微服务治理能力(如:限流、熔断、降级、服务发现、负载均衡、链路追踪等)帮助开发者快速构建可靠、可扩展的分布式系统。go-zero 提供了极简的 API 定义语言和强大的配套工具 goctl ,开发者只需编写 .api 或 .proto 文件,即可一键生成工程级代码(包括业务逻辑、HTTP 路由、RPC 调用、数据库操作、配置管理、错误处理等),大幅提升开发效率的同时保证了代码规范统一。此外,go-zero 内置了自适应熔断器、自动缓存管理、服务监控、分布式事务等高级特性,并经过大规模线上服务检验,是云原生时代快速构建高并发微服务的理想选择。

1.1.2 工程级代码框架
根据 go-zero 的简介及其代码生成工具 goctl 的功能,通过编写 .api 或 .proto 文件一键生成的工程代码通常包含以下详细内容:
(1) 业务逻辑层
-
核心业务处理代码:根据 API 定义生成对应的业务逻辑接口,开发者只需填充具体的实现(如调用 RPC、操作数据库、计算等)。
-
参数校验:自动生成请求参数的校验逻辑(如必填字段、类型、范围等),保证进入业务层的数据合法。
-
响应封装 :统一封装返回格式(如
code、msg、data),方便前端解析。
(3)HTTP 路由层
-
路由注册 :根据
.api文件中定义的路径和方法(如GET /user/:id)自动生成路由表,将请求映射到对应的处理函数。 -
中间件集成:支持自定义中间件(如鉴权、日志、限流),生成的代码中预留中间件挂载点。
-
参数绑定:自动将 HTTP 请求中的 Query 参数、表单数据、JSON 体绑定到结构体,供业务层使用。
(3) RPC 调用层
-
客户端代码:生成调用下游 RPC 服务的客户端 stub,简化服务间通信。
-
服务端代码:生成 RPC 服务的基础框架,包括接口定义、请求处理入口。
-
负载均衡 与服务发现:集成内置的负载均衡策略,自动从注册中心(如 etcd、Nacos)获取服务地址。
远程过程调用 协议( Remote Procedure Call , RPC ) ,它是一种客户端/服务器之间进行通信和交互的协议。它允许一个程序在另一个计算机上执行代码,就像在本地计算机上执行一样。通过RPC,可以在不同的系统上执行操作,并且可以轻松地扩展应用程序,从而简化了计算机系统之间的通信,使得不同平台上的应用程序能够相互交流和协作。
(4)数据库操作层
-
数据模型(Model) :根据数据库表结构或
.proto中的 message 定义生成对应的 Go 结构体。 -
CRUD 基础代码:生成针对单表的增、删、改、查方法,支持条件查询、分页、事务等。
-
缓存管理:内置自动缓存控制逻辑,可配置缓存策略(如失效时间、Key 规则),减少数据库压力。
(5) 配置管理
-
配置文件解析:生成默认的配置文件(如 YAML 或 TOML 格式),包含服务端口、数据库连接、Redis 地址等常用配置项。
-
配置结构体 :定义与配置文件对应的 Go 结构体,并提供加载方法(如
LoadConfig()),支持热加载。 -
环境变量 覆盖:支持通过环境变量覆盖配置,便于容器化部署。
(6)错误处理与日志
-
统一错误码:生成预定义的错误码和错误信息,便于国际化或统一响应。
-
错误包装:提供错误包装函数,自动记录堆栈信息,方便排查问题。
-
日志记录:集成日志库(如 logx),在请求入口、RPC 调用、数据库操作等关键位置自动打印结构化日志。
(7)其他工程化内容
-
启动入口(main.go):生成服务启动文件,包含信号监听、优雅退出等逻辑。
-
单元测试示例:为生成的代码提供基础的测试骨架,方便开发者编写测试用例。
-
Dockerfile 与构建脚本:可选生成容器化部署所需的文件。
-
API 文档:根据注释或定义自动生成 Swagger 文档,便于团队协作。
通过 goctl 生成的工程代码严格遵循 go-zero 的设计规范,既保证了代码的一致性,又让开发者能够专注于核心业务逻辑的实现,大幅提升开发效率和系统的稳定性。
1.2.3 go-zero 高级特性:
-
自适应熔断器:这是一种智能保护机制,它会持续监控服务调用的成功率、响应时间等指标,当检测到下游服务出现故障或延迟过高时,自动触发熔断,暂时切断对该服务的请求,避免故障向上游蔓延,同时让下游有时间恢复;熔断器会根据系统负载和恢复情况动态调整熔断阈值,在稳定性和可用性之间自动取得平衡,无需人工干预。
-
自动缓存管理:该特性指框架内置了完整的缓存解决方案,开发者只需通过简单的注解或配置,即可对数据库查询结果、RPC 响应等数据进行缓存;框架会自动处理缓存的写入、失效、更新和防穿透等逻辑,并支持多级缓存(如本地缓存 + Redis),大大减少重复代码,提升系统吞吐量和响应速度。
-
服务监控:服务监控模块会自动收集微服务的各项运行指标,包括请求量、错误率、响应延迟、CPU/内存使用率等,并将这些数据上报至监控系统(如 Prometheus);同时支持链路追踪,能够串联一次请求经过的所有服务节点,帮助开发者快速定位性能瓶颈和异常,并支持配置告警规则,在指标异常时及时通知运维人员。
-
分布式 事务:在跨多个微服务或数据库的业务操作中,分布式事务确保所有参与的操作要么全部成功提交,要么全部失败回滚,从而保证最终数据一致性;go-zero 内置了基于消息队列的柔性事务方案(如 TCC、Saga),并提供了简单易用的 API,让开发者可以像操作本地事务一样实现分布式协调,无需关心底层的复杂逻辑。
**go-zero官网:**https://go-zero.dev/
1.2 go-zero在WEB3.0中的应用场景
go-zero 在 Web3.0 中的应用场景非常广泛,主要集中在构建链下服务层 和去中心化应用(DApp)的后端基础设施 。Web3.0 应用虽然核心逻辑运行在区块链上,但依然需要大量的链下服务来处理链上无法高效完成 或成本过高的任务,例如数据索引、用户管理、API 聚合、实时行情推送、与智能合约的高频交互等。go-zero 作为一个集成了微服务治理能力的框架,恰好能满足这些场景对高性能、高可用、可扩展的需求。下面将从几个核心场景详细展开说明。
1.2.1 数据索引与缓存服务(The Graph 类替代方案)
在 Web3.0 中,区块链本身是一个只追加的分布式账本,查询历史数据(如某个地址的所有交易)非常困难且缓慢。通常需要构建数据 索引服务 ,将链上事件和交易数据同步到高性能数据库中(如 PostgreSQL 、Redis ),并提供 RESTful 或 GraphQL API 供前端查询。
go-zero 的作用:
-
高并发数据同步: 使用 go-zero 的 RPC 服务监听区块链节点的事件(如新块、新交易),通过
go-eth或web3.go解析事件,并将数据写入数据库。go-zero 的并发模型(基于 goroutine)可以高效处理大量事件。 -
API 服务: 定义
.api文件,通过goctl自动生成索引数据的查询接口,提供给前端或第三方应用。go-zero 内置的缓存管理(如自动缓存失效)可以大幅提升查询性能。 -
**微服务拆分:**可以将索引服务拆分为多个微服务,例如"区块服务"、"交易服务"、"ERC20 转账服务",各自独立部署和扩展,通过 go-zero 的 service discovery 进行通信。
示例架构:
1.2.2 去中心化应用的 API 网关
Web3.0 前端 DApp 通常需要与多个链下服务交互:用户认证(钱包签名验证)、链上数据查询、交易构建与广播等。一个统一的 API 网关可以简化前端逻辑,并提供安全防护。
go-zero 的优势:
-
极简 API 定义 :使用
goctl快速定义 API 接口,自动生成路由、参数校验、文档,减少重复劳动。 -
中间件集成:可以轻松添加鉴权中间件(如验证 JWT 或 Ethereum 签名)、限流中间件(防止恶意请求)、日志中间件。
-
服务聚合:网关层可以将多个下游微服务的响应聚合成一个响应返回给前端,减少网络往返。
典型流程:
(1)前端发起请求,附带用户钱包地址和签名。
(2)go-zero 网关中间件验证签名,确认用户身份。
(3)网关将请求路由到对应的微服务(如用户服务、资产服务)。
(4)微服务执行逻辑(可能查询数据库或调用智能合约),返回结果。
(5)网关聚合响应,返回给前端。
1.2.3 与智能合约的高效交互层
DApp 后端经常需要与智能合约交互,例如调用合约的 view 方法查询数据,或构造交易并签名发送。go-zero 可以封装这些交互为内部 RPC 服务,供其他服务调用。
具体实现:
-
合约封装: 使用
go-ethereum生成合约绑定代码(通过abigen),然后在 go-zero 的 RPC 服务中提供方法,如GetBalance、Transfer。 -
**批处理优化:**利用 go-zero 的并发特性,将多个合约调用批量化,减少 RPC 请求次数。
-
**重试与熔断:**调用区块链节点可能失败(如节点不稳定),go-zero 内置的熔断和重试机制可以自动处理这些问题,提高健壮性。
示例代码片段(RPC 服务定义):
Go
// 语法声明:使用 Protocol Buffers 的 proto3 版本
syntax = "proto3";
// 包名定义,用于避免命名冲突,生成的 Go 代码的包名将以此为基础
package contract;
// ContractService 定义了与智能合约交互的 gRPC 服务
// 该服务封装了常见的区块链合约操作,如查询余额和发送交易
service ContractService {
// GetBalance 查询指定地址的账户余额
// 请求: GetBalanceRequest 包含要查询的地址
// 响应: GetBalanceResponse 返回该地址的余额
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse);
// SendTransaction 构造并发送一笔交易
// 请求: SendTransactionRequest 包含交易参数(如接收方地址、金额、签名等)
// 响应: SendTransactionResponse 返回交易哈希或状态
rpc SendTransaction(SendTransactionRequest) returns (SendTransactionResponse);
}
// 注意:上述请求和响应消息类型(GetBalanceRequest, GetBalanceResponse 等)
// 需要在同一文件或导入的文件中另行定义,例如:
// message GetBalanceRequest {
// string address = 1;
// }
// message GetBalanceResponse {
// string balance = 1; // 可能用字符串表示大数
// }
通过 goctl 生成代码后,只需在业务逻辑中调用合约方法即可。
1.2.4 数字货币交易平台的核心后端
对于中心化交易所(CEX)或去中心化交易所(DEX)的订单簿部分(链下订单簿),go-zero 可以完美胜任。
关键模块:
-
订单撮合引擎:使用 go-zero 的并发安全特性,构建高性能的订单撮合引擎,处理限价单、市价单的撮合逻辑。
-
实时行情推送:通过 WebSocket(go-zero 支持 WebSocket)向客户端推送实时 K 线、盘口深度、最新成交等信息。
-
用户资产管理:管理用户的充提记录、余额冻结等,与链上资产进行对账。
go-zero 的优势:
-
分布式事务:在涉及资金的操作中,需要保证数据一致性。go-zero 可以集成分布式事务方案(如 Saga、TCC)或使用本地事务 + 消息队列的最终一致性。
-
服务监控 :内置的
prometheus指标和日志可以帮助实时监控系统状态,及时发现异常。 -
水平扩展:微服务架构使得订单、用户、行情等服务可以独立水平扩展,应对高并发流量。
1.2.5 跨链桥(Bridge)的后端服务
跨链桥需要监听多个链的事件,并执行跨链交易。go-zero 可以构建多个链的监听服务,并协调复杂的跨链流程。
实现思路:
-
每个链对应一个 go-zero 的监听服务,订阅该链的特定事件(如存款事件)。
-
当事件发生时,监听服务将信息上报给核心协调服务(也是 go-zero 实现)。
-
协调服务验证事件有效性,然后调用目标链的合约执行 mint 操作。
-
整个过程需要保证原子性(要么两边都成功,要么都回滚),可以使用 go-zero 的分布式事务中间件或 Saga 模式。
1.2.6 Web3.0 社交平台的后端
对于去中心化社交平台(如类似 Lens Protocol 的应用),链上存储核心数据(如 Profile、Follow 关系),而链下服务负责内容分发、推荐算法、消息推送等。
go-zero 的应用:
-
内容存储:将用户的帖子、评论等存储在 IPFS 或 Arweave,但索引和缓存使用 go-zero 构建的 API 服务。
-
社交图谱分析:通过 go-zero 的计算服务,定期分析链上的 Follow 关系,生成推荐关注列表。
-
即时通讯:使用 WebSocket 或 MQTT 实现用户之间的消息传递,go-zero 可以处理大量长连接。
总结: go-zero 在 Web3.0 中的核心定位是链下服务的基石。它通过提供一套完整的微服务解决方案,帮助开发者快速构建高性能、可扩展、易维护的后端系统,从而弥补区块链在计算、存储和实时性方面的不足。无论是数据索引、合约交互、API 网关还是复杂的交易平台,go-zero 都能通过其简洁的 API 定义、强大的代码生成和丰富的治理能力,显著提升开发效率并保障线上稳定性。
1.3 go-zero 和 Gin 对比
| 对比维度 | go-zero | Gin |
|---|---|---|
| 核心定位 | 面向微服务的全栈式框架 | 轻量级 Web 框架、高性能、简洁易用 |
| 核心用途 | 构建高性能 API 接口与微服务 | 构建高性能、Web 应用(API 和传统页面) |
| 设计哲学 | "工具大于约定"、工程化、标准化 | 灵活简洁、高性能路由 |
| 治理能力 | 内置(限流、熔断、服务发现等) | 依赖中间件(需集成第三方) |
| 开发效率 | 高(提供 good 工具链生成代码) | 中(灵活但需手动完成更多工作) |
| 协议支持 | 同时支持 HTTP 和 RPC (gRPC) | 主要支持 HTTP/HTTPS |
| 适用场景 | 构建 API 和大型复杂的微服务项目 | 快速构建 API 或构建需要模板渲染的 Web 应用 |
| 发布日期 | 2020年8月 | 2014年 |
go-zero 和 Gin 并非替代关系,而是适用于不同场景的框架。go-zero 是一个功能完备的微服务框架,集成了服务发现、熔断、限流、链路追踪等治理能力,旨在解决分布式系统的复杂性问题,适合构建大规模微服务架构。而 Gin 是一个轻量级的 Web 框架,专注于 HTTP 层的快速开发和简洁性,性能高且易于上手,非常适合快速构建单体应用、RESTful API 或作为微服务中的单个服务。当项目复杂度较低、无需微服务治理时,Gin 的轻量级优势更明显;当需要构建高可用、可扩展的分布式系统时,go-zero 的一体化解决方案则更具优势。
1.4 Go-zero特点
go-zero 是一个集成了微服务治理能力的 Web 和 RPC 框架,它的核心设计目标是帮助开发者轻松构建高并发、高可用的分布式系统。它具备以下几个突出特点:
(1)高稳定性保障:内置了级联超时控制、自适应熔断、限流、降载等一系列微服务治理能力,即使面对千万级日活流量也能自动保护服务,无需手动配置或编写额外代码,就能确保系统稳定运行。
(2)极简开发体验 :通过强大的 goctl 工具,只需用极简的 API 描述文件定义接口,即可一键生成前后端代码、参数校验逻辑、数据库操作等工程化代码,大幅提升开发效率,降低出错可能。
(3)全面的故障应对:采用"面向故障编程"的设计理念,服务发现、负载均衡、熔断降级等机制自动触发并自动恢复,同时内置链路追踪、统计报警功能,让系统在异常时能自我修复,保障业务连续性。
(4)高性能与兼容性 :完全兼容 Go 标准库**net/http**,支持中间件扩展,运行性能极高;同时提供丰富的并发工具包和自动缓存控制,帮助开发者轻松应对高并发场景。
这些特性使得 go-zero 在 Web3.0 领域被广泛应用于构建去中心化交易所(DEX)的链下订单撮合引擎 、NFT 市场的实时行情推送服务 以及 DeFi 协议的合约数据索引后端,稳定支撑了千万级用户的交易请求和链上事件处理,充分验证了其在真实业务场景下的可靠性。对于零基础的开发者来说,你可以理解为:go-zero 是一个"开箱即用"的微服务框架,它把复杂分布式系统需要的能力都内置好了,你只需要专注于写业务逻辑即可。
链下订单撮合引擎:这是一种运行在传统服务器上的软件系统,专门为去中心化交易所(DEX)提供订单匹配服务。由于区块链网络处理速度有限且费用较高,将订单薄存放在链下并由该引擎实时计算买卖双方的匹配结果,可以大幅提升交易效率并降低成本。它负责接收用户提交的买卖订单,按照价格优先、时间优先的原则进行自动撮合,同时维护订单队列的实时状态,最终将成交结果广播到链上完成结算。
NFT市场的实时行情推送服务 :在NFT交易平台中,该服务负责收集、计算并向用户实时推送各类市场数据,如最低价格(地板价)、交易量、最新成交价等。它通常通过监听区块链上NFT合约的转账事件或聚合多个链下数据源,利用**
WebSocket**等长连接技术,将数据变化以极低延迟推送给前端用户,帮助投资者及时了解市场动态并做出决策。DeFi协议的合约数据索引后端:该后端系统专门用于解析和存储DeFi智能合约中产生的大量链上数据,以提供高效的查询接口。由于区块链数据难以直接进行复杂检索,索引后端会实时监听合约事件,将交易、借贷、流动性池状态等关键信息提取出来,并组织到结构化的数据库(如PostgreSQL)中。这样,用户或前端应用就能通过REST API快速查询历史记录、用户持仓、协议总锁仓量等指标,而无需直接遍历区块链。

第二章:go-zero安装
2.1 go-zero安装
2.1.1 安装 goctl
goctl 是 go-zero 框架提供的强大代码生成工具,它通过解析用户定义的 .api 或 .proto 文件,能够一键生成从项目工程骨架、HTTP 路由、RPC 调用、数据库模型(Model)、参数校验逻辑、错误处理到 Swagger 文档等全套工程化代码,同时支持生成 JavaScript、Java 等多种语言的客户端 SDK,大幅提升微服务开发效率,确保项目结构统一规范,让开发者只需专注于业务逻辑实现即可。
安装:在命令行窗口执行如下命令
Go
go install github.com/zeromicro/go-zero/tools/goctl@latest

验证:在命令行窗口执行如下命令
Go
goctl --version

2.1.2 安装 protoc
protoc 是 Protocol Buffers(protobuf)的编译器,它的核心作用是将定义好的 .proto 文件编译成指定编程语言的源代码,用于高效的数据序列化和服务接口定义。在 go-zero 的实践中,你通常不会直接单独使用 protoc,而是通过 goctl rpc protoc 命令来间接调用它。这个命令会组合 protoc 以及两个关键的 Go 语言插件:protoc-gen-go (用于生成消息结构体代码)和 protoc-gen-go-grpc (用于生成 gRPC 服务代码)。最终,goctl 会将这些基础代码与 go-zero 的微服务框架整合,一键生成包含配置、逻辑层、服务注册在内的完整工程化 RPC 服务。值得一提的是,go-zero 也提供了 goctl env check --install 命令,可以帮你一键安装 protoc 及其相关插件,简化了环境配置的步骤。
**安装:**在命令行窗口执行如下命令即可完成 protoc 的安装。
Go
goctl env check --install --verbose --force
这两个命令都是用于检查和安装 go-zero 开发所需的环境依赖(如
protoc、protoc-gen-go等)。(1)
goctl env check --install会静默执行环境检查,若发现缺失组件则自动安装;(2)
goctl env check --install --verbose --force则在检查时输出详细的日志信息(--verbose),并强制覆盖或重新安装已有组件(--force),适用于需要调试或确保环境绝对最新的场景。

验证:在命令行窗口执行如下命令;
Go
goctl env

2.1.3 配置 VS Code 插件
goctl VS Code 指的是 Visual Studio Code 编辑器中专为 go-zero 框架开发的官方插件,它通过在编辑器中提供图形化辅助功能,极大地提升了使用 goctl 工具的开发体验。这个插件像一座桥梁,将 goctl 命令行工具的强大能力无缝集成到 VSCode 的图形界面中,让开发者编写 .api 文件时能享受到以下便利:
(1)语法高亮:让 .api 文件中的关键字、字段等以不同颜色显示,结构一目了然。
(2)代码跳转:支持在定义和引用之间快速跳转,方便理解代码逻辑。
(3)代码格式化:一键调用 goctl 命令,将 .api 文件内容整理为标准格式,保持代码风格统一。
(4)代码块提示:输入关键词时,自动弹出预定义的代码片段模板(snippets),提高编码速度和准确性。
在使用该插件前,你需要确保已经成功安装了 goctl 命令行工具 ,并将其所在目录添加到了系统的环境变量 PATH 中。

2.2 创建初始化项目
在 go-zero 框架中,执行 goctl api new firstdemo 命令用于快速创建一个名为 firstdemo 的全新 API 服务项目,它会自动生成包括主程序、配置文件、路由和业务逻辑在内的标准化工程结构。
2.2.1 创建项目
Go
goctl api new firstdemo

2.2.2 初始化项目
cd 进入到刚创建的项目中
Go
cd firstdemo

下载依赖
Go
go mod tidy
2.2.3 目录介绍
查看当前工作目录的详细目录结构,以获取完整的项目文件信息。
Go
d:\workspace\go-zero\firstdemo/
├── etc/ 存放配置文件
│ └── firstdemo-api.yaml API服务配置文件
├── firstdemo.api API接口定义文件
├── firstdemo.go 程序主入口文件
├── go.mod Go模块依赖定义
├── go.sum Go模块依赖校验
└── internal/ 项目的核心代码
├── config/ 配置管理模块
│ └── config.go 配置结构体和加载逻辑
├── handler/ 请求处理层
│ ├── firstdemohandler.go 具体接口处理实现
│ └── routes.go 路由注册配置
├── logic/ 业务逻辑层
│ └── firstdemologic.go 核心业务逻辑实现
├── svc/ 服务上下文层
│ └── servicecontext.go 依赖注入和服务上下文
└── types/ 数据类型定义
└── types.go 请求响应结构体定义
go-zero项目结构,包含以下主要部分:
(1)etc/ 目录:存放配置文件
(2)internal/ 目录:包含项目的核心代码
-
config/ :配置相关
-
handler/ :HTTP处理器
-
logic/ :业务逻辑
-
svc/ :服务上下文
-
types/ :数据类型定义
(3)firstdemo.api :API定义文件
(4)firstdemo.go :主程序入口
(5)go.mod 和 go.sum :Go模块依赖管理文件
2.3 修改参数运行项目
2.3.1 编写业务逻辑 运行项目
(1)编写业务逻辑
在logic目录下的 firstdemologic.go 中写入如下代码
Go
func (l *FirstdemoLogic) Firstdemo(req *types.Request) (resp *types.Response,err error) {
// todo: add your logic here and delete this line
resp = &types.Response{
Message: "hello " + req.Name,
}
return resp, nil
}

(2)运行项目
Go
go run .\firstdemo.go
(3)访问项目:打开如下的网址。
Go
http://localhost:8888//from/you
返回如下的内容:

(4)修改传参
地址:
Go
http://localhost:8888/from/zhangsan
返回

goctl api new xxx 可以快捷生成一个最小化的http服务,但我们希望 form 后面的name可以任意输入,就需要修改代码。更改 firstdemo.api文件内容。
Go
syntax = "v1"
type Request {
Name string `path:"name,options=you|me"`
}
type Response {
Message string `json:"message"`
}
service firstdemo-api {
@handler FirstdemoHandler
get /from/:name (Request) returns (Response)
}
options=you|me 表示该路径参数 name 只能接受两个特定的值:
-
"you"
-
"me"
去掉options
Go
type Request {
Name string `path:"name"`
}
重新生成代码,运行命令
Go
goctl api go --api firstdemo.api --dir .
# --api :指定api文件
# --dir :指定go文件生成的目录
goctl api go --api firstdemo.api --dir .是 go-zero 框架提供的代码生成命令,它的作用是根据指定的firstdemo.api接口定义文件,在当前目录(.)下生成完整的 Go 服务端工程代码,包括路由注册、请求处理、业务逻辑骨架、配置文件等,从而快速搭建起一个符合 go-zero 规范的 API 服务,省去手动编写基础代码的繁琐过程。其中--api指定 API 描述文件,--dir指定代码输出的目标目录。当你需要根据
.api文件生成或更新 go-zero 项目的服务端代码时,就需要运行该命令。常见场景包括:
新建项目: 首次创建 API 服务,写好
firstdemo.api后,用此命令生成初始工程结构。**修改接口定义:**增删改 API 路由、请求/响应结构体后,重新运行命令以同步更新代码。
团队协作: 拉取到他人提交的
.api文件变更后,执行命令确保本地代码与最新接口定义一致。该命令能避免手动编写重复代码,保证项目结构规范统一。

重新运行访问即可
地址
Go
http://localhost:8888/from/zhangsan
返回

2.3.2 goctl api 常用命令详解
Go
PS D:\workspace\go_zero_demo\zerozpi> goctl api --help
Generate api related files
Usage:
goctl api [flags]
goctl api [command]
Available Commands:
dart Generate dart files for provided api in api file
doc Generate doc files
format Format api files
go Generate go files for provided api in api file
kt Generate kotlin code for provided api file
new Fast create api service
plugin Custom file generator
swagger Generate swagger file from api
ts Generate ts files for provided api in api file
validate Validate api file
(1)goctl api new 快速创建 API 服务(必须记住)
goctl api new 是 go-zero 框架提供的快速创建 API 服务的命令,它会在当前目录下生成一个包含标准化工程结构的新项目,包括 API 定义文件、主程序入口、配置文件、路由注册、业务逻辑层等基础代码,让开发者无需从零搭建项目结构,直接开始编写业务逻辑,从而大幅提升开发效率。
Go
goctl api new firstdemo
(2)goctl api format 格式化 api 文件
goctl api format 是 go-zero 框架提供的 API 文件格式化命令,用于自动调整 .api 接口描述文件的代码风格,包括统一缩进、空格、换行以及按规范排序结构体字段和路由信息,使文件格式整洁一致,避免手动调整格式的繁琐工作,并帮助团队保持统一的代码风格,提升可读性和维护效率。
Go
goctl api format --dir .
(3)goctl api go为 api 文件生成 Go 代码(必须记住)
goctl api go 是 go-zero 框架的核心命令,用于根据 .api 接口定义文件生成完整的 Go 语言服务端代码,包括 HTTP 路由注册、请求参数绑定、响应封装、业务逻辑层骨架以及配置文件等,是开发者将 API 设计转化为可运行服务的关键步骤,极大简化了从接口定义到代码实现的流程。
Go
goctl api go --api shop.api --dir .
(4)goctl api doc 生成doc文档
goctl api doc 是 go-zero 框架提供的API文档生成命令,用于根据 .api 接口定义文件自动生成接口文档。它的核心作用是将 API 定义转换为便于阅读的格式------默认生成 Markdown 文档,列出所有接口的路由、请求参数、响应结构等信息;结合实验性功能也可生成 Swagger(OpenAPI)规范的 JSON/YAML 文档,进而通过 Swagger UI 提供可视化的交互式接口文档和测试页面。该命令常用于团队协作时向前后端成员提供清晰、与代码同步的接口说明,或作为项目交付物的一部分。
Go
goctl api doc --dir . --o ./doc
(5)goctl api dart/goctl api ts 生成调用文件
goctl api dart 和 goctl api ts 是 go-zero 框架提供的客户端代码生成命令,分别用于根据 .api 接口定义文件生成 Dart 和 TypeScript 语言的 HTTP 请求封装代码。这些命令会自动创建包含所有 API 路由、请求参数和响应结构的客户端模块,让移动端(Flutter/Dart)或 Web 端(TypeScript)开发者可以直接调用对应方法发起网络请求,无需手动编写重复的请求逻辑,从而保证前后端接口定义同步,大幅提升开发效率和协作体验。
Go
goctl api dart --api shop.api --dir ./dart
goctl api ts --api shop.api --dir ./ts
(6)goctl api swagger
goctl api swagger 是 go-zero 框架提供的命令,用于根据 .api 接口定义文件生成 Swagger(OpenAPI)格式的 JSON 文档,该文档可以被 Swagger UI 或其他工具加载,从而生成可视化、可交互的 API 接口文档和测试页面,方便前后端协作和接口调试,是提升团队沟通效率和文档规范性的重要工具。
Go
goctl api swagger --api shop.api --dir ./docs --filename shop
要让**Swagger** 加载你生成的 shop.json 文件,有几种主流且方便的方法,你可以根据自己的开发环境和喜好来选择。下面这个表格汇总了常见的方案:
| 方法 | 适用场景 | 核心步骤 |
|---|---|---|
| Docker容器 | 快速启动、环境隔离 | 1. 运行特定Docker命令; 2. 将JSON文件挂载到容器内 |
| Node.js本地服务 | 本地开发、已有Node环境 | 1. 下载Swagger UI; 2. 用http-server启动 |
| 集成到现有项目 | 项目已使用特定Web框架 | 1. 引入Swagger UI库; 2. 配置静态文件路由 |
(7)goctl api 生成项目的风格
go-zero生成项目后默认所有的单词连到一起,要改变风格可以通过--style实现。
Go
# 默认
goctl api go --api shop.api --dir .
# 生成蛇形命名(推荐,更清晰)
goctl api go --api shop.api --dir . --style=go_zero
# 或者生成驼峰命名
goctl api go --api shop.api --dir . --style=goZero
| 样式选项 | 命名风格 | 示例文件名 |
|---|---|---|
| gozero | 默认风格,全小写并连写 | getarticlecatehandler.go |
| go_zero | 蛇形命名,使用下划线分隔(推荐) | get_article_cate_handler.go |
| goZero | 驼峰命名 | getArticleCateHandler.go |
2.4 热加载简介
所谓热加载就是当我们对代码进行修改时,程序能够自动重新加载并执行,这在我们开发中是非常便利 4.1
的,可以快速进行代码测试,省去了每次手动重新编译。
工具1:https://github.com/gravityblast/fresh
Go
go install github.com/pilu/fresh@latest
Go
fresh
**工具2:**https://github.com/air-verse/air
Go
go install github.com/air-verse/air@latest
Go
ari
第三章:api 概述
api 是 go-zero 团队自主研发的领域特定语言(DSL),用于简洁、高效地描述 HTTP 服务的接口定义。它以人为中心设计,旨在成为生成服务端代码的基础描述语言,降低开发者编写重复代码的成本。
api 语言由几个核心语法单元组成: 语法版本声明、info 信息块、数据结构声明(与 Go 结构体语法几乎一致,仅省略了 struct 关键字)以及服务描述(定义路由和对应的处理方法)。通过这些声明,开发者可以完整定义 API 的请求、响应格式及访问路径,配合 goctl 工具一键生成工程化代码,实现接口定义与代码实现的分离。
语法版本声明: 用于指定
.api文件遵循的语法规范版本,确保goctl工具正确解析。**info 信息块:**用于描述 API 的元数据(如标题、版本、作者),便于生成文档和辅助理解。
**数据结构声明:**定义请求和响应的数据类型(类似 Go 结构体),明确接口的输入输出格式。
**服务描述:**定义路由、HTTP 方法、处理器名称以及关联的请求/响应结构,将接口映射到具体业务逻辑。
3.1 请求参数
在 go-zero 的 api 领域特定语言中,请求参数通过在结构体字段上使用标签(如 json 、form 、path 、query )来定义,这些标签指明了参数在 HTTP 请求中的具体位置------例如 JSON 请求体、表单数据、URL 路径或查询字符串,同时字段类型决定了参数的格式。这种声明方式让接口输入规范一目了然,配合 goctl 工具可自动生成参数绑定、类型转换和校验代码,开发者无需手动解析请求,大幅提升开发效率和接口健壮性。
3.1.1 参数介绍
在 api 描述语言中,我们可以通过在 tag 中来声明参数接收规则,目前 go-zero 支持的参数接收规则如下:
| 接收规则 | 说明 | 生效范围 | 接收tag示例 | 请求示例 |
|---|---|---|---|---|
| json | json 序列化 | 请求体 & 响应体 | json:"foo" |
{"key":"value"} |
| path | 路由参数 | 请求体 | path:"id" |
/foo/:id |
| form | post 请求的表单(支持 content-type 为 form-data 和 x-www-form-urlencoded)参数接收标识;get 请求的 query 参数接收标识 | 请求体 | form:"name" |
GET /search?key=value |
| header | http 请求头接收标识 | 请求体 | header:"Content-Length" |
origin: https://go-zero.dev |
3.1.2 可空参数
在 go-zero 的 api 领域特定语言中,可空参数指请求中允许不传递或值为空的字段,通过在结构体字段前添加 optional 关键字或使用指针类型(如 *int)来标记。生成 Go 代码后,该字段会变为指针类型,并自动在 JSON 标签中加入 omitempty,从而能清晰区分"字段未传"和"字段传了零值"两种场景,避免因零值(如 0、false)无法判断是否赋值而引发的逻辑错误,使接口定义更灵活、健壮。
omitempty 表示可空参数(返回的字段可不设置) json:"articles,omitempty";
Go
syntax = "v1"
type CommonResponse {
Message string `json:"message"`
Code int `json:"code"`
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
}
optional 表示接收数据的时候可以为空;
Go
UpdateFocusRequest {
Id int `form:"id"` //Put传值
Title string `form:"title,optional"`
Pic string `form:"pic,optional"`
Link string `form:"link,optional"`
Position int `form:"position,optional"`
}
goctl命令生成api
Go
goctl api --o shop.api
goctl api --o shop.api命令用于在当前目录下生成一个名为shop.api的示例 API 描述文件,该文件包含 go-zero 框架的 api 领域特定语言的基本结构(如语法版本、info 信息、数据结构声明和服务定义),作为开发者编写接口定义的起始模板,方便快速上手并规范 API 设计。

(1)Get请求
HTTP GET 请求是用于从服务器获取资源的标准方法,其特点是将请求参数通过 URL 传递------可以以查询字符串(?key=value)的形式附加在 URL 末尾,或作为路径参数嵌入 URL 路径中。由于参数暴露在 URL 中,GET 请求不适合传输敏感信息,且 URL 长度通常有限制(各浏览器和服务器有不同上限)。GET 请求是幂等的,即多次相同的请求不会改变服务器的状态,返回的资源结果也应相同(除非资源本身发生变化)。在 go-zero 的 api 语言中,GET 请求的参数通常使用 query 或 path 标签来定义,分别对应查询字符串和路径参数。
Go
// 指定 api 语言的语法版本,v1 表示使用 go-zero 最新的 DSL 规范
syntax = "v1"
// CommonResponse 定义通用的 API 响应结构体,所有接口返回的统一格式
type CommonResponse {
// 提示信息,例如 "success" 或错误描述
Message string `json:"message"`
// 业务状态码,通常 0 表示成功,非 0 表示错误
Code int `json:"code"`
// 是否成功,true 表示成功,false 表示失败
Success bool `json:"success"`
// 实际返回的数据,类型不定,使用 interface{} 表示任意类型
// omitempty 表示如果字段为空值(nil),则 JSON 中不包含该字段
Data interface{} `json:"data,omitempty"`
}
// 使用括号可以一次性声明多个类型,提高可读性
type (
// FocusRequest 定义获取轮播图列表的请求参数
FocusRequest {
// Id 为轮播图 ID,通过 GET 请求的查询参数传递(form 标签)
// 注意:虽然这里是 form 标签,但结合下方 service 中 get 方法,实际是查询参数
Id int64 `form:"id"`
}
// Focus 定义轮播图的详细信息结构体
Focus {
Id int64 `json:"id"` // 轮播图 ID
Name string `json:"name"` // 轮播图标题
Pic string `json:"pic"` // 图片 URL
Url string `json:"url"` // 点击后跳转的链接
}
)
// service 块定义 HTTP 服务的路由和处理方式
service shop-api {
// @handler 指定该路由对应的 Go 处理方法名(由 goctl 生成)
@handler GetFocusList
// 定义 GET 请求,路径为 /api/focus,接收 FocusRequest 参数,返回 CommonResponse
get /api/focus (FocusRequest) returns (CommonResponse)
}
注释说明:
-
开头的
syntax = "v1"声明使用的 api 语言版本,类似 Protocol Buffers 的语法。 -
CommonResponse作为通用响应封装,所有接口都返回此格式,包含状态码、消息、数据和成功标志。 -
FocusRequest中的form:"id"标签指示该参数从 HTTP 请求的表单数据中获取,但由于这是 GET 请求,实际会从查询字符串(query string)中解析,go-zero 的form标签同时支持 GET 的查询参数和 POST 的表单参数。 -
Focus结构体定义了轮播图的数据结构,json标签指定 JSON 序列化时的字段名。 -
service块定义路由和处理器映射,get /api/focus (FocusRequest) returns (CommonResponse)表示这是一个 GET 请求,路径为/api/focus,请求参数类型为FocusRequest,响应类型为CommonResponse。@handler后面的名称用于生成对应的 Go 函数名。
该文件是 go-zero 框架的核心输入,通过 goctl 工具可以生成对应的服务端代码。
生成代码
Go
goctl api go --api shop.api --dir .
请求方式
Go
http://localhost:8888/api/focus?id=20
(2)POST请求
POST 是 HTTP 协议中用于向服务器发送数据以创建或更新资源的方法,其请求参数通常包含在请求体中(而非 URL),因此更适合传输敏感信息和大数据量,且没有长度限制;POST 请求是非幂等的,即多次相同的请求可能产生不同的结果(如重复提交订单),常用于表单提交、文件上传、RESTful API 中创建资源等需要修改服务器状态的场景。
Go
// 指定 api 语言的语法版本为 v1,这是 go-zero 当前使用的 DSL 规范版本
syntax = "v1"
// CommonResponse 定义统一的 API 响应格式,所有接口返回的数据都将遵循此结构
type CommonResponse {
// Message 包含操作结果的文本信息,如 "success" 或错误描述
Message string `json:"message"`
// Code 业务状态码,通常 0 表示成功,非零值表示具体错误码
Code int `json:"code"`
// Success 表示操作是否成功,true 成功,false 失败
Success bool `json:"success"`
// Data 存放实际返回的数据,类型不固定,使用 interface{} 表示任意类型
// omitempty 表示如果 Data 为 nil 或零值,则在 JSON 中忽略此字段
Data interface{} `json:"data,omitempty"`
}
// 使用括号批量定义多个类型,使代码更紧凑
type (
// FocusRequest 定义获取轮播图列表的请求参数
FocusRequest {
// Id 轮播图 ID,通过 GET 请求的查询参数传递(form 标签用于查询参数)
Id int64 `form:"id"` // 例如:/api/focus?id=123
}
// AddFocusRequest 定义添加轮播图的请求参数(POST 请求)
AddFocusRequest {
// Name 轮播图标题,通过 POST 表单字段传递
Name string `form:"name"`
// Pic 轮播图图片 URL,通过 POST 表单字段传递
Pic string `form:"pic"`
// Url 点击轮播图跳转的链接,通过 POST 表单字段传递
Url string `form:"url"`
}
// Focus 定义轮播图的数据结构,用于返回给客户端
Focus {
Id int64 `json:"id"` // 轮播图唯一标识
Name string `json:"name"` // 轮播图标题
Pic string `json:"pic"` // 图片地址
Url string `json:"url"` // 跳转链接
}
)
// service 块定义 HTTP 服务的路由和处理函数映射
service shop-api {
// @handler 指定生成代码中的 Go 函数名,该处理函数对应下面的 GET 请求
@handler GetFocusList
// 定义 GET 请求,路径为 /api/focus,使用 FocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
get /api/focus (FocusRequest) returns (CommonResponse)
// @handler 指定另一个处理函数名,对应 POST 请求
@handler AddFocus
// 定义 POST 请求,路径为 /api/focus/add,使用 AddFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
post /api/focus/add (AddFocusRequest) returns (CommonResponse)
}
生成代码
Go
goctl api go --api shop.api --dir .
请求方式
Go
http://localhost:8888/api/focus/add

(3)PUT请求
PUT 是 HTTP 协议中用于向服务器发送数据以更新或替换指定资源的方法,其请求参数通常包含在请求体中,要求客户端发送完整的资源表示,服务器会用该表示替换目标资源;PUT 是幂等的,即多次执行相同的 PUT 请求会产生相同的结果,不会因重复请求而改变资源状态,这与 POST(非幂等,用于创建)形成关键区别,常用于 RESTful API 中更新已有资源(如修改用户信息)。
Go
// 指定 api 语言的语法版本为 v1,这是 go-zero 当前使用的 DSL 规范版本
syntax = "v1"
// CommonResponse 定义统一的 API 响应格式,所有接口返回的数据都将遵循此结构
type CommonResponse {
// Message 包含操作结果的文本信息,如 "success" 或错误描述
Message string `json:"message"`
// Code 业务状态码,通常 0 表示成功,非零值表示具体错误码
Code int `json:"code"`
// Success 表示操作是否成功,true 成功,false 失败
Success bool `json:"success"`
// Data 存放实际返回的数据,类型不固定,使用 interface{} 表示任意类型
// omitempty 表示如果 Data 为 nil 或零值,则在 JSON 中忽略此字段
Data interface{} `json:"data,omitempty"`
}
// 使用括号批量定义多个类型,使代码更紧凑
type (
// FocusRequest 定义获取焦点图列表的请求参数
// 对应 GET 请求,参数通过 URL 查询字符串传递(form 标签在 GET 中表示查询参数)
FocusRequest {
Id int64 `form:"id"` // 焦点图 ID,通过查询参数传递,例如:/api/focus?id=123
}
// AddFocusRequest 定义添加焦点图的请求参数
// 对应 POST 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
AddFocusRequest {
Name string `form:"name"` // 焦点图标题
Pic string `form:"pic"` // 焦点图图片 URL
Url string `form:"url"` // 点击焦点图跳转的链接
}
// EditFocusRequest 定义编辑焦点图的请求参数
// 对应 PUT 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
EditFocusRequest {
Id int64 `form:"id"` // 要编辑的焦点图 ID
Name string `form:"name"` // 新的标题
Pic string `form:"pic"` // 新的图片 URL
Url string `form:"url"` // 新的跳转链接
}
// Focus 定义焦点图的数据结构,用于返回给客户端(如查询结果列表中的每一项)
Focus {
Id int64 `json:"id"` // 焦点图唯一标识
Name string `json:"name"` // 焦点图标题
Pic string `json:"pic"` // 图片地址
Url string `json:"url"` // 跳转链接
}
)
// service 块定义 HTTP 服务的路由和处理函数映射
service shop-api {
// @handler 指定生成代码中的 Go 函数名,该处理函数对应下面的 GET 请求
@handler GetFocusList
// 定义 GET 请求,路径为 /api/focus,使用 FocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应(其中 Data 字段可能包含 Focus 列表)
get /api/focus (FocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 POST 请求
@handler AddFocus
// 定义 POST 请求,路径为 /api/focus/add,使用 AddFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
post /api/focus/add (AddFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 PUT 请求
@handler EditFocus
// 定义 PUT 请求,路径为 /api/focus/edit,使用 EditFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
put /api/focus/edit (EditFocusRequest) returns (CommonResponse)
}
生成代码
Go
goctl api go --api shop.api --dir .
请求地址:
Go
http://localhost:8888/api/focus/edit

(4)Delete请求
DELETE 是 HTTP 协议中用于删除指定资源的方法,其请求通常不包含请求体,而是通过 URL 路径或查询参数指定要删除的资源标识(如 /api/focus/123)。DELETE 是幂等的,即多次执行相同的 DELETE 请求结果相同------首次删除后资源不再存在,后续请求虽然返回可能不同(如 404),但服务器状态不会再次改变,这使其区别于 POST,常用于 RESTful API 中移除用户、文章等资源。
Go
// 指定 api 语言的语法版本为 v1,这是 go-zero 当前使用的 DSL 规范版本
syntax = "v1"
// CommonResponse 定义统一的 API 响应格式,所有接口返回的数据都将遵循此结构
type CommonResponse {
// Message 包含操作结果的文本信息,如 "success" 或错误描述
Message string `json:"message"`
// Code 业务状态码,通常 0 表示成功,非零值表示具体错误码
Code int `json:"code"`
// Success 表示操作是否成功,true 成功,false 失败
Success bool `json:"success"`
// Data 存放实际返回的数据,类型不固定,使用 interface{} 表示任意类型
// omitempty 表示如果 Data 为 nil 或零值,则在 JSON 中忽略此字段
Data interface{} `json:"data,omitempty"`
}
// 使用括号批量定义多个类型,使代码更紧凑
type (
// FocusRequest 定义获取焦点图列表的请求参数
// 对应 GET 请求,参数通过 URL 查询字符串传递(form 标签在 GET 中表示查询参数)
FocusRequest {
Id int64 `form:"id"` // 焦点图 ID,通过查询参数传递,例如:/api/focus?id=123
}
// AddFocusRequest 定义添加焦点图的请求参数
// 对应 POST 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
AddFocusRequest {
Name string `form:"name"` // 焦点图标题
Pic string `form:"pic"` // 焦点图图片 URL
Url string `form:"url"` // 点击焦点图跳转的链接
}
// EditFocusRequest 定义编辑焦点图的请求参数
// 对应 PUT 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
EditFocusRequest {
Id int64 `form:"id"` // 要编辑的焦点图 ID
Name string `form:"name"` // 新的标题
Pic string `form:"pic"` // 新的图片 URL
Url string `form:"url"` // 新的跳转链接
}
// Focus 定义焦点图的数据结构,用于返回给客户端(如查询结果列表中的每一项)
Focus {
Id int64 `json:"id"` // 焦点图唯一标识
Name string `json:"name"` // 焦点图标题
Pic string `json:"pic"` // 图片地址
Url string `json:"url"` // 跳转链接
}
)
// service 块定义 HTTP 服务的路由和处理函数映射
service shop-api {
// @handler 指定生成代码中的 Go 函数名,该处理函数对应下面的 GET 请求
@handler GetFocusList
// 定义 GET 请求,路径为 /api/focus,使用 FocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应(其中 Data 字段可能包含 Focus 列表)
get /api/focus (FocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 POST 请求
@handler AddFocus
// 定义 POST 请求,路径为 /api/focus/add,使用 AddFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
post /api/focus/add (AddFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 PUT 请求
@handler EditFocus
// 定义 PUT 请求,路径为 /api/focus/edit,使用 EditFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
put /api/focus/edit (EditFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 DELETE 请求
@handler DeleteFocus
// 定义 DELETE 请求,路径为 /api/focus/delete,使用 FocusRequest 作为请求参数,
// 注意:DELETE 请求通常不包含请求体,这里的参数通过查询字符串传递(form 标签),
// 例如:DELETE /api/focus/delete?id=123,用于删除指定 ID 的焦点图
// 返回 CommonResponse 格式的响应
delete /api/focus/delete (FocusRequest) returns (CommonResponse)
}
生成代码
Go
goctl api go --api shop.api --dir .
请求地址:
Go
http://localhost:8888/api/focus/delete?id=12

(5)path参数(动态路由)
在 HTTP API 设计中,path参数(动态路由) 是指将变量直接嵌入 URL 路径中,用于标识具体资源的一种方式,例如 /user/:id 中的 :id 就是一个动态参数,实际请求时会被具体的值替换(如 /user/123)。这种设计符合 RESTful 风格,能够清晰表达资源层级关系,常用于定位单个资源(如查询、更新、删除)。在 go-zero 的 api 语言中,通过 path 标签定义,例如 Id int64path:"id"``,框架会自动从 URL 路径中解析出该参数,开发者无需手动解析字符串,简化了代码编写并提升了接口的可读性和规范性。
Go
// 指定 api 语言的语法版本为 v1,这是 go-zero 当前使用的 DSL 规范版本
syntax = "v1"
// CommonResponse 定义统一的 API 响应格式,所有接口返回的数据都将遵循此结构
type CommonResponse {
// Message 包含操作结果的文本信息,如 "success" 或错误描述
Message string `json:"message"`
// Code 业务状态码,通常 0 表示成功,非零值表示具体错误码
Code int `json:"code"`
// Success 表示操作是否成功,true 成功,false 失败
Success bool `json:"success"`
// Data 存放实际返回的数据,类型不固定,使用 interface{} 表示任意类型
// omitempty 表示如果 Data 为 nil 或零值,则在 JSON 中忽略此字段
Data interface{} `json:"data,omitempty"`
}
// 使用括号批量定义多个类型,使代码更紧凑
type (
// ----- 焦点图相关请求/响应结构体 -----
// FocusRequest 定义获取焦点图列表的请求参数
// 对应 GET 请求,参数通过 URL 查询字符串传递(form 标签在 GET 中表示查询参数)
FocusRequest {
Id int64 `form:"id"` // 焦点图 ID,通过查询参数传递,例如:/api/focus?id=123
}
// AddFocusRequest 定义添加焦点图的请求参数
// 对应 POST 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
AddFocusRequest {
Name string `form:"name"` // 焦点图标题
Pic string `form:"pic"` // 焦点图图片 URL
Url string `form:"url"` // 点击焦点图跳转的链接
}
// EditFocusRequest 定义编辑焦点图的请求参数
// 对应 PUT 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
EditFocusRequest {
Id int64 `form:"id"` // 要编辑的焦点图 ID
Name string `form:"name"` // 新的标题
Pic string `form:"pic"` // 新的图片 URL
Url string `form:"url"` // 新的跳转链接
}
// Focus 定义焦点图的数据结构,用于返回给客户端(如查询结果列表中的每一项)
Focus {
Id int64 `json:"id"` // 焦点图唯一标识
Name string `json:"name"` // 焦点图标题
Pic string `json:"pic"` // 图片地址
Url string `json:"url"` // 跳转链接
}
)
// 第二个括号定义文章相关的请求/响应结构体
type (
// ArticleRequest 定义获取单篇文章的请求参数
// 对应 GET 请求,参数通过 URL 路径中的动态路由传递(path 标签)
ArticleRequest {
Id int64 `path:"id"` // 文章 ID,从 URL 路径中获取,例如 /api/article/123 中的 123
}
// Article 定义文章的数据结构,用于返回给客户端
Article {
Id int64 `json:"id"` // 文章唯一标识
Title string `json:"title"` // 文章标题
Content string `json:"content"` // 文章内容
CreateAt int64 `json:"create_at"` // 创建时间戳(Unix 时间戳)
}
)
// service 块定义 HTTP 服务的路由和处理函数映射
service shop-api {
// @handler 指定生成代码中的 Go 函数名,该处理函数对应下面的 GET 请求
@handler GetFocusList
// 定义 GET 请求,路径为 /api/focus,使用 FocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应(其中 Data 字段可能包含 Focus 列表)
get /api/focus (FocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 POST 请求
@handler AddFocus
// 定义 POST 请求,路径为 /api/focus/add,使用 AddFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
post /api/focus/add (AddFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 PUT 请求
@handler EditFocus
// 定义 PUT 请求,路径为 /api/focus/edit,使用 EditFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
put /api/focus/edit (EditFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 DELETE 请求
@handler DeleteFocus
// 定义 DELETE 请求,路径为 /api/focus/delete,使用 FocusRequest 作为请求参数,
// 注意:DELETE 请求通常不包含请求体,这里的参数通过查询字符串传递(form 标签),
// 例如:DELETE /api/focus/delete?id=123,用于删除指定 ID 的焦点图
// 返回 CommonResponse 格式的响应
delete /api/focus/delete (FocusRequest) returns (CommonResponse)
// 获取文章列表(单篇文章)
// @handler 指定处理函数名,对应 GET 请求
@handler GetOneArticle
// 定义 GET 请求,路径为 /api/article/:id,使用 ArticleRequest 作为请求参数,
// 其中 :id 是动态路由,会被 path 标签解析到 ArticleRequest.Id 字段
// 返回 CommonResponse 格式的响应(Data 字段可能包含 Article 对象)
get /api/article/:id (ArticleRequest) returns (CommonResponse) // 动态路由
}
访问:
Go
http://localhost:8888/api/article/212
3.1.3 参数默认值与参数枚举值
在 go-zero 的 api 领域特定语言中,参数默认值是指当请求未提供该字段时,系统会自动使用预先设定的值作为参数,通过 default 标签实现(如 form:"name,default=张三"),从而简化客户端调用并保证数据完整性;参数枚举值则通过 options 标签限定参数只能从指定的几个候选值中选择(如 form:"type,options=hot|new|recommend"),当传入值不在枚举范围内时自动触发校验失败,有效防止非法参数传入,提升接口健壮性和规范性。
Go
type (
AddUserRequest {
Id int64 `form:"id"`
Name string `form:"name"`
Age int64 `form:"age,default=18"` //参数默认值
Sex int64 `form:"sex,option=1|2"` //参数枚举值
}
)
Go
// 指定 api 语言的语法版本为 v1,这是 go-zero 当前使用的 DSL 规范版本
syntax = "v1"
// CommonResponse 定义统一的 API 响应格式,所有接口返回的数据都将遵循此结构
type CommonResponse {
// Message 包含操作结果的文本信息,如 "success" 或错误描述
Message string `json:"message"`
// Code 业务状态码,通常 0 表示成功,非零值表示具体错误码
Code int `json:"code"`
// Success 表示操作是否成功,true 成功,false 失败
Success bool `json:"success"`
// Data 存放实际返回的数据,类型不固定,使用 interface{} 表示任意类型
// omitempty 表示如果 Data 为 nil 或零值,则在 JSON 中忽略此字段
Data interface{} `json:"data,omitempty"`
}
// 焦点图相关请求/响应结构体
type (
// FocusRequest 定义获取焦点图列表的请求参数
// 对应 GET 请求,参数通过 URL 查询字符串传递(form 标签在 GET 中表示查询参数)
FocusRequest {
Id int64 `form:"id"` // 焦点图 ID,通过查询参数传递,例如:/api/focus?id=123
}
// AddFocusRequest 定义添加焦点图的请求参数
// 对应 POST 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
AddFocusRequest {
Name string `form:"name"` // 焦点图标题
Pic string `form:"pic"` // 焦点图图片 URL
Url string `form:"url"` // 点击焦点图跳转的链接
}
// EditFocusRequest 定义编辑焦点图的请求参数
// 对应 PUT 请求,参数通过请求体中的表单字段传递(form 标签表示表单字段)
EditFocusRequest {
Id int64 `form:"id"` // 要编辑的焦点图 ID
Name string `form:"name"` // 新的标题
Pic string `form:"pic"` // 新的图片 URL
Url string `form:"url"` // 新的跳转链接
}
// Focus 定义焦点图的数据结构,用于返回给客户端(如查询结果列表中的每一项)
Focus {
Id int64 `json:"id"` // 焦点图唯一标识
Name string `json:"name"` // 焦点图标题
Pic string `json:"pic"` // 图片地址
Url string `json:"url"` // 跳转链接
}
)
// 文章相关请求/响应结构体
type (
// ArticleRequest 定义获取单篇文章的请求参数
// 对应 GET 请求,参数通过 URL 路径中的动态路由传递(path 标签)
ArticleRequest {
Id int64 `path:"id"` // 文章 ID,从 URL 路径中获取,例如 /api/article/123 中的 123
}
// Article 定义文章的数据结构,用于返回给客户端
Article {
Id int64 `json:"id"` // 文章唯一标识
Title string `json:"title"` // 文章标题
Content string `json:"content"` // 文章内容
CreateAt int64 `json:"create_at"` // 创建时间戳(Unix 时间戳)
}
)
// 用户相关请求/响应结构体,演示参数默认值和枚举值用法
type (
// AddUserRequest 定义添加用户的请求参数
// 对应 POST 请求,参数通过表单字段传递,并演示默认值和枚举值
AddUserRequest {
Id int64 `form:"id"` // 用户 ID(必传)
Name string `form:"name"` // 用户名(必传)
Age int64 `form:"age,default=18"` // 年龄,默认值 18,如果请求未提供则使用默认值
Sex int64 `form:"sex,options=1|2"` // 性别,枚举值,只能为 1 或 2,否则触发校验错误
}
// User 定义用户的数据结构,用于返回给客户端
User {
Id int64 `json:"id"` // 用户唯一标识
Name string `json:"name"` // 用户名
Age int64 `json:"age"` // 年龄
Sex int64 `json:"sex"` // 性别:1 男,2 女
}
)
// service 块定义 HTTP 服务的路由和处理函数映射
service shop-api {
// @handler 指定生成代码中的 Go 函数名,该处理函数对应下面的 GET 请求
@handler GetFocusList
// 定义 GET 请求,路径为 /api/focus,使用 FocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应(其中 Data 字段可能包含 Focus 列表)
get /api/focus (FocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 POST 请求
@handler AddFocus
// 定义 POST 请求,路径为 /api/focus/add,使用 AddFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
post /api/focus/add (AddFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 PUT 请求
@handler EditFocus
// 定义 PUT 请求,路径为 /api/focus/edit,使用 EditFocusRequest 作为请求参数,
// 返回 CommonResponse 格式的响应
put /api/focus/edit (EditFocusRequest) returns (CommonResponse)
// @handler 指定处理函数名,对应 DELETE 请求
@handler DeleteFocus
// 定义 DELETE 请求,路径为 /api/focus/delete,使用 FocusRequest 作为请求参数,
// 注意:DELETE 请求通常不包含请求体,这里的参数通过查询字符串传递(form 标签),
// 例如:DELETE /api/focus/delete?id=123,用于删除指定 ID 的焦点图
// 返回 CommonResponse 格式的响应
delete /api/focus/delete (FocusRequest) returns (CommonResponse)
// 获取文章列表(单篇文章)
// @handler 指定处理函数名,对应 GET 请求
@handler GetOneArticle
// 定义 GET 请求,路径为 /api/article/:id,使用 ArticleRequest 作为请求参数,
// 其中 :id 是动态路由,会被 path 标签解析到 ArticleRequest.Id 字段
// 返回 CommonResponse 格式的响应(Data 字段可能包含 Article 对象)
get /api/article/:id (ArticleRequest) returns (CommonResponse)
// 增加用户
// @handler 指定处理函数名,对应 POST 请求
@handler AddUser
// 定义 POST 请求,路径为 /api/user/add,使用 AddUserRequest 作为请求参数,
// 该请求中包含默认值和枚举值字段,框架会自动处理
// 返回 CommonResponse 格式的响应
post /api/user/add (AddUserRequest) returns (CommonResponse)
}
业务逻辑
Go
// AddUser 是添加用户的业务逻辑处理函数
// 它接收一个 AddUserRequest 指针作为输入,返回 CommonResponse 指针和错误信息
// 该函数由 goctl 根据 api 定义自动生成,开发者需在此实现具体业务逻辑
func (l *AddUserLogic) AddUser(req *types.AddUserRequest) (resp *types.CommonResponse, err error) {
// 使用 go-zero 内置的 logx 记录请求日志,便于调试和监控
// 这里记录添加的用户名,req.Name 来自客户端请求
logx.Info("添加用户:", req.Name)
// 构造成功响应,返回给客户端
// CommonResponse 是统一的响应格式,包含 Code、Message 和 Data 字段
// 这里将请求参数中的字段直接赋值给 User 结构体,作为 Data 返回
// 注意:实际业务中可能需要对数据进行持久化(如存入数据库)后再返回
return &types.CommonResponse{
Code: 200, // 业务状态码,200 表示成功
Message: "添加用户成功", // 提示信息
Data: types.User{ // 实际返回的用户数据
Id: req.Id, // 用户 ID,从请求参数中获取
Name: req.Name, // 用户名,从请求参数中获取
Age: req.Age, // 年龄,从请求参数中获取(可能带有默认值)
Sex: req.Sex, // 性别,从请求参数中获取(受枚举值限制)
},
}, nil // 返回 nil 错误表示操作成功
}

3.2 编写一个获取轮播图列表的Api服务
3.2.1 新建shop.api
第一步:创建 shop.api 文件
Go
# 新创建一个文件夹
mkdir seconddemo
# 进入到文件夹中
cd seconddemo
# 创建一个shop.api文件
goctl api -o shop.api
第二步:将如下代码放入到 shop.api 文件中
Go
syntax = "v1"
// 定义接口
type (
// 定义登录接口的响应体
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
// 定义 HTTP 服务
service shop {
// 定义 http.HandleFunc 转换的 go 文件名称及方法
@handler GetFocusList
// 定义接口
// 请求方法为 post
// 路由为 /user/login
// 请求体为 LoginReq
// 响应体为 LoginResp,响应体必须有 returns 关键字修饰
get /focus returns (FocusResp)
}
service 语句是对 HTTP 服务的直观描述,包含请求 handler,请求方法,请求路由,请求体,响应体,jwt 开关,中间件声明等定义。
3.2.2 生成项目
Go
goctl api go --api shop.api --dir .
-
**goctl :**go-zero 框架的命令行工具
-
**api go :**表示要生成 Go 语言的 API 服务代码
-
**--api shop.api :**指定 API 定义文件为 shop.api
-
**--dir . :**指定生成代码的输出目录为当前目录
运行该命令会根据 shop.api 文件自动生成如下的内容:
(1)API 路由配置
作用 :就像一本"地址簿",它记录了客户端可以通过哪些网址(比如 /api/focus)访问你的服务,以及每个网址应该对应哪个处理函数。
理解:当用户访问某个网址时,路由配置会告诉程序:"这个请求应该交给谁处理"。
(2)Handler 处理函数
作用:每个路由对应的"接待员",负责接收请求、提取参数、调用业务逻辑,然后把结果返回给客户端。
理解:它负责"开门迎客",但不负责具体的业务处理,只是把工作交给下一层,最后把结果包装好送出去。
(3)Logic 业务逻辑层
作用:真正的"干活的地方",在这里实现具体的业务逻辑,比如查询数据库、调用外部接口、计算数据等。
理解:这是代码的核心部分,所有与业务相关的规则和操作都在这里完成。
(4)Main 入口文件
作用:程序的"启动器",负责创建服务、加载配置、注册路由和中间件,最后启动 HTTP 服务器。
理解:就像汽车的点火开关,所有组件都是在这里被组装起来并开始运行的。
(5)配置文件
作用:存放程序运行时需要的配置信息,如服务端口、数据库地址、日志级别等,方便在不修改代码的情况下改变设置。
理解:就像家里的电闸开关,你可以在这里调整参数,而不需要拆开电器改电线。
(6)中间件等基础设施
作用:在请求进入正式业务逻辑之前或之后,自动执行的一些通用功能,如权限校验、日志记录、超时控制、跨域处理等。
理解:相当于一个"安检通道",每个请求都要先经过它(比如检查是否登录),通过后才能进入业务逻辑。
这样分层的好处是:各司其职,互不干扰。你只需要在 Logic 层写业务代码,其他部分由工具自动生成,开发效率大大提高。
3.2.3 下载依赖
Go
go mod tidy

3.2.4 修改业务代码
修改**E:\web3_study\golang_study\seconddemo** **\internal\logic\getfocuslistlogic.go**文件。将文件中的函数 GetFocusList 更改为下述的代码。
Go
func (l *GetFocusListLogic) GetFocusList() (resp *types.FocusResp, err error) {
//返回一个模拟的轮播图列表数据
focusList := []*types.Focus{
{
Id: 1,
Name: "轮播图1",
Pic: "https://picsum.photos/200/300",
Position: 1,
Url: "https://www.baidu.com",
},
{
Id: 2,
Name: "轮播图2",
Pic: "https://picsum.photos/200/300",
Position: 2,
Url: "https://www.baidu.com",
},
}
return &types.FocusResp{
Result: focusList,
}, nil
}
注意: 在实际项目中,业务代码应该从数据库或其他持久化存储中获取真实数据,而不是直接写死。图一中的示例只是为了演示接口返回格式,在实际开发中,需要将
GetFocusList方法中的硬编码数据替换为从数据库查询的结果。go-zero 项目通常会在internal/model层定义数据模型和数据库操作,然后在logic层中调用这些 model 方法来获取数据,从而实现数据的动态化和持久化。
3.2.5 访问项目
Go
# 运行代码
fresh
# 访问网址
http://localhost:8888/focus
输出如下内容:

postman调用输出内容:

3.3 在上面的项目中加入获取文章列表的Api服务(GET传值、动态路由)
3.3.1 修改shop.api
Go
syntax = "v1"
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
service shop-api {
@handler GetOneFocus
get /api/focus/details/:id (OneFocusReq) returns (OneFocusResp)
@handler GetFocusList
get /api/focus returns (FocusResp)
@handler GetOneArticle
get /api/article/details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /api/article returns (ArticleResp)
}
3.3.2 生成项目
Go
goctl api go --api shop.api --dir .
3.3.3 修改业务代码
更改 E:\web3_study\golang_study\seconddemo \internal\logic\getonefocuslogic.go 中的**GetOneFocus**函数,将其替换成如下的代码内容。
Go
func (l *GetOneFocusLogic) GetOneFocus(req *types.OneFocusReq) (resp *types.OneFocusResp, err error) {
// todo: add your logic here and delete this line
//获取轮播图id
id := req.Id
//模拟返回这个id的轮播图数据
return &types.OneFocusResp{
Result: types.Focus{
Id: id,
Name: "轮播图1",
Pic: "https://picsum.photos/200/300",
Url: "https://www.baidu.com",
Position: 1,
},
}, nil
}
修改 E:\web3_study\golang_study\seconddemo\ internal\logic\getfocuslistlogic.go 文件中的**GetFocusList**函数代码,将如下的业务逻辑替换至函数中。
Go
func (l *GetFocusListLogic) GetFocusList() (resp *types.FocusResp, err error) {
// todo: add your logic here and delete this line
//返回一个模拟的轮播图列表数据
focusList := []*types.Focus{
{
Id: 1,
Name: "轮播图1",
Pic: "https://picsum.photos/200/300",
Position: 1,
Url: "https://www.baidu.com",
},
{
Id: 2,
Name: "轮播图2",
Pic: "https://picsum.photos/200/300",
Position: 2,
Url: "https://www.baidu.com",
},
}
return &types.FocusResp{
Result: focusList,
}, nil
}
修改 E:\web3_study\golang_study\seconddemo \internal\logic\getonearticlelogic.go 文件中的**GetOneArticle** 函数,使用下述的代码进行替换。
Go
func (l *GetOneArticleLogic) GetOneArticle(req *types.OneArticleReq) (resp *types.OneArticleResp, err error) {
// todo: add your logic here and delete this line
//获取文章id
id := req.Id
//模拟返回这个id的文章数据
resp = &types.OneArticleResp{
Result: types.Article{
Id: id,
Title: "文章标题1",
Description: "文章描述1",
Pic: "https://pic.downk.cc/item/5f5c0c5c5b7f9c7f9c5c5c5c.jpg",
Content: "文章内容",
},
}
return resp, nil
}
修改 E:\web3_study\golang_study\seconddemo \internal\logic\getarticlelistlogic.go 文件中的**GetArticleList**函数,使用如下的代码进行替换。
Go
func (l *GetArticleListLogic) GetArticleList() (resp *types.ArticleResp, err error) {
// todo: add your logic here and delete this line
//返回一个模拟的文章列表数据
resp = &types.ArticleResp{
Result: []*types.Article{
{
Id: 1,
Title: "文章1",
Description: "文章1的描述",
Pic: "https://picsum.photos/200/300",
Content: "文章1的内容",
},
{
Id: 2,
Title: "文章2",
Description: "文章2的描述",
Pic: "https://picsum.photos/200/300",
},
},
}
return resp, nil
}
3.3.4 访问项目
Go
# 运行项目
fresh
# 访问网址
http://localhost:8888/api/focus
Go
{
"result": [
{
"id": 1,
"name": "轮播图1",
"pic": "https://picsum.photos/200/300",
"url": "https://www.baidu.com",
"position": 1
},
{
"id": 2,
"name": "轮播图2",
"pic": "https://picsum.photos/200/300",
"url": "https://www.baidu.com",
"position": 2
}
}
http://localhost:8888/api/focus/details/124 (动态路由)
Go
{
"result": {
"id": 124,
"name": "轮播图1",
"pic": "https://picsum.photos/200/300",
"url": "https://www.baidu.com",
"position": 1
}
}
http://localhost:8888/api/article
Go
{
"result": [
{
"id": 1,
"title": "文章1",
"description": "文章1的描述",
"pic": "https://picsum.photos/200/300",
"content": "文章1的内容"
},
{
"id": 2,
"title": "文章2",
"description": "文章2的描述",
"pic": "https://picsum.photos/200/300",
"content": ""
}
]
}
http://localhost:8888/api/article/details?id=12
Go
{
"result": {
"id": 12,
"title": "文章标题1",
"description": "文章描述1",
"pic": "https://pic.downk.cc/item/5f5c0c5c5b7f9c7f9c5c5c5c.jpg",
"content": "文章内容"
}
}
3.4 @server 配置前缀以及对项目进行分组
在 go-zero 的 .api 文件中,@server 就像一个"群组设置",可以为一组接口统一配置一些公共属性,比如:
-
网址前缀: 给这些接口的地址前面统一加一个段落,例如
/v1,这样所有接口的完整地址就变成了/v1/...。 -
**通用功能:**为这组接口统一添加一些共用的功能,比如"检查用户是否登录"或"记录请求日志",避免在每个接口里重复写这些代码。
这样一来,文件更整洁,修改时也只需改一处,省时省力。对于零基础的朋友,可以理解为:给一群接口贴上一张"共同标签",告诉程序它们有共同的规则。
3.4.1 修改shop.api
Go
syntax = "v1"
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
@server (
group: focus // 代表当前 service 代码块下的路由生成代码时都会被放到 focus 目录下
prefix: /api/focus
)
service shop-api {
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
@handler GetFocusList
get /list returns (FocusResp)
}
@server (
group: article // 代表当前 service 代码块下的路由生成代码时都会被放到 article 目录下
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
@server语句是对一个服务语句的 meta 信息描述,其对应特性包含但不限于:jwt 开关、中间件、路由分组、路由前缀。
3.4.2 生成项目
Go
goctl api go --api shop.api --dir .
提示:新生成项目后,以前的代码会保留,需要手动删除。如果以前代码中有业务逻辑的话,需要手动替换新生成的代码。
3.4.3 修改业务代码
修改 E:\web3_study\golang_study\seconddemo \internal\logic\focus\getonefocuslogic.go 文件中的**GetOneFocus**函数,将其替换成下面内容。
Go
func (l *GetOneFocusLogic) GetOneFocus(req *types.OneFocusReq) (resp *types.OneFocusResp, err error) {
// todo: add your logic here and delete this line
//获取轮播图id
id := req.Id
//模拟返回这个id的轮播图数据
return &types.OneFocusResp{
Result: types.Focus{
Id: id,
Name: "轮播图1",
Pic: "https://picsum.photos/200/300",
Url: "https://www.baidu.com",
Position: 1,
},
}, nil
}
修改 E:\web3_study\golang_study\seconddemo \internal\logic\focus\getfocuslistlogic.go 文件中的**GetFocusList**函数,将其替换成如下的内容。
Go
func (l *GetFocusListLogic) GetFocusList() (resp *types.FocusResp, err error) {
// todo: add your logic here and delete this line
//返回一个模拟的轮播图列表数据
focusList := []*types.Focus{
{
Id: 1,
Name: "轮播图1",
Pic: "https://picsum.photos/200/300",
Position: 1,
Url: "https://www.baidu.com",
},
{
Id: 2,
Name: "轮播图2",
Pic: "https://picsum.photos/200/300",
Position: 2,
Url: "https://www.baidu.com",
},
}
return &types.FocusResp{
Result: focusList,
}, nil
}
修改 E:\web3_study\golang_study\seconddemo\internal\logic\article\getonearticlelogic.go 文件中的 GetOneArticle 函数,将其替换成如下的内容。
Go
func (l *GetOneArticleLogic) GetOneArticle(req *types.OneArticleReq) (resp *types.OneArticleResp, err error) {
// todo: add your logic here and delete this line
//获取文章id
id := req.Id
//模拟返回这个id的文章数据
resp = &types.OneArticleResp{
Result: types.Article{
Id: id,
Title: "文章标题1",
Description: "文章描述1",
Pic:
"https://pic.downk.cc/item/5f5c0c5c5b7f9c7f9c5c5c5c.jpg",
Content: "文章内容",
},
}
return resp, nil
}
修改 E:\web3_study\golang_study\seconddemo\internal\logic\article\getarticlelistlogic.go文件中的 GetArticleList函数,将其替换成下面的代码。
Go
func (l *GetArticleListLogic) GetArticleList() (resp *types.ArticleResp, err error) {
// todo: add your logic here and delete this line
//返回一个模拟的文章列表数据
resp = &types.ArticleResp{
Result: []*types.Article{
{
Id: 1,
Title: "文章1",
Description: "文章1的描述",
Pic: "https://picsum.photos/200/300",
Content: "文章1的内容",
},
{
Id: 2,
Title: "文章2",
Description: "文章2的描述",
Pic: "https://picsum.photos/200/300",
},
},
}
return resp, nil
}
3.4.4 访问项目
http://localhost:8888/api/focus/list
Go
{
"result": [
{
"id": 1,
"name": "轮播图1",
"pic": "https://picsum.photos/200/300",
"url": "https://www.baidu.com",
"position": 1
},
{
"id": 2,
"name": "轮播图2",
"pic": "https://picsum.photos/200/300",
"url": "https://www.baidu.com",
"position": 2
}
]
}
http://localhost:8888/api/focus/details/124 (动态路由)
Go
{
"result": {
"id": 124,
"name": "轮播图1",
"pic": "https://picsum.photos/200/300",
"url": "https://www.baidu.com",
"position": 1
}
}
http://localhost:8888/api/article/list
Go
{
"result": [
{
"id": 1,
"title": "文章1",
"description": "文章1的描述",
"pic": "https://picsum.photos/200/300",
"content": "文章1的内容"
},
{
"id": 2,
"title": "文章2",
"description": "文章2的描述",
"pic": "https://picsum.photos/200/300",
"content": ""
}
]
}
http://localhost:8888/api/article/details?id=12
Go
{
"result": {
"id": 12,
"title": "文章标题1",
"description": "文章描述1",
"pic": "https://pic.downk.cc/item/5f5c0c5c5b7f9c7f9c5c5c5c.jpg",
"content": "文章内容"
}
}
3.5 编写带有中间件的 api 服务
在 go-zero 中编写带有中间件的 API 服务,首先需要在 .api 文件中通过 @server 配置块声明中间件,例如 @server middleware: AuthMiddleware,然后在项目中的 middleware 目录下实现该中间件(一个符合 func(next http.HandlerFunc) http.HandlerFunc 签名的函数),最后在 main.go 或 servicecontext.go 中通过 router.Use() 或路由组的方式将中间件注册到服务中。这样,所有经过该路由的请求都会先执行中间件逻辑(如鉴权、日志记录),再进入业务处理,实现了横切关注点的统一管理。
3.5.1 修改shop.api
Go
syntax = "v1"
// Focus的请求参数
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
// 文章的请求参数
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
//用户的请求参数
type (
GetUserInfoReq {
Id int64 `form:"id"` // 使用 form tag 表示 x-www-form-urlencoded 数据
}
GetUserInfoResp {
Id int64 `json:"id"`
Name string `json:"name"`
Desc string `json:"desc"`
}
)
//轮播图的服务
@server (
group: focus
prefix: /api/focus
)
service shop-api {
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
@handler GetFocusList
get /list returns (FocusResp)
}
//文章的服务
@server (
group: article
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
//用户的服务
@server (
group: user
prefix: /api/user
// 定义一个鉴权控制的中间件,多个中间件以英文逗号,分割,如 Middleware1,Middleware2,中间件按声明顺序执行
middleware: AuthInterceptor
)
service shop-api {
@handler GetUserInfo
post /info (GetUserInfoReq) returns (GetUserInfoResp)
}
3.5.2 生成项目
Go
goctl api go --api shop.api --dir .
3.5.3 完善中间件代码
实现中间件逻辑 authinterceptormiddleware.go
修改**E:\web3_study\golang_study\deconddemo2** \internal\middleware\authinterceptormiddleware.go 文件中的 **Handle**结构体方法。将替换成下面的内容。
Go
func (m *AuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO generate middleware implement function, delete after code
implementation
token := r.Header.Get("Authorization")
if token == "" {
// 认证失败,返回 401 而不是 panic
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("未授权访问"))
return
}
log.Println("认证通过,继续执行...")
// Passthrough to next handler if need
next(w, r)
}
}
修改 E:\web3_study\golang_study\deconddemo2 \internal\svc\servicecontext.go 文件内容为如下的代码。
Go
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package svc
import (
"apidemo01/internal/config"
"apidemo01/internal/middleware"
"github.com/zeromicro/go-zero/rest"
)
type ServiceContext struct {
Config config.Config
UserAgentMiddleware rest.Middleware // 自定义中间件属性
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserAgentMiddleware: middleware.NewAuthInterceptorMiddleware().Handle,
//注册中间件
}
}
3.5.4 路由中注册中间件
修改 E:\web3_study\golang_study\deconddemo2 \internal\handler\routes.go 文件中的**RegisterHandlers**为如下的内容。
Go
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
...
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.UserAgentMiddleware}, //注册中间件
[]rest.Route{
{
Method: http.MethodPost,
Path: "/info",
Handler: user.GetUserInfoHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/user"),
)
}
修改 E:\web3_study\golang_study\deconddemo2 \internal\logic\user\getuserinfologic.go 文件中的 **GetUserInfo **结构体方法为下面的内容。
模拟返回数据
Go
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (resp *types.GetUserInfoResp, err error) {
// todo: add your logic here and delete this line
return &types.GetUserInfoResp{
Desc: "用户信息",
Id: req.Id,
Name: "张三",
}, nil
}
访问请求地址
Go
http://localhost:8888/api/user/info


3.6 编写带有超时配置的 api 服务
在 go-zero 中,编写带有超时配置的 API 服务,只需在 .api 文件的 @server 块中通过 timeout 关键字声明超时时间(如 timeout: 5s),或在配置文件(如 etc/shop-api.yaml)中设置 Timeout 字段,即可为所有接口或特定路由组自动启用超时控制,确保请求在指定时间内完成,提升服务的健壮性。
3.6.1 配置
Go
@server (
timeout: 3s
)
timeout: 3s 表示:
(1)整个HTTP请求处理的超时时间为3秒
(2)如果从请求开始到响应完成的总时间超过3秒,服务端会自动断开连接
(3)这包括业务逻辑处理时间、数据库查询时间、网络延迟等所有环节
完整代码
Go
syntax = "v1"
// Focus的请求参数
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
// 文章的请求参数
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
//用户的请求参数
type (
GetUserInfoReq {
Id int64 `form:"id"` // 使用 form tag 表示 x-www-form-urlencoded 数据
}
GetUserInfoResp {
Id int64 `json:"id"`
Name string `json:"name"`
Desc string `json:"desc"`
}
)
//轮播图的服务
@server (
group: focus
prefix: /api/focus
timeout: 3s //配置超时时间3秒
)
service shop-api {
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
@handler GetFocusList
get /list returns (FocusResp)
}
//文章的服务
@server (
group: article
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
//用户的服务
@server (
group: user
prefix: /api/user
// 定义一个鉴权控制的中间件,多个中间件以英文逗号,分割,如 Middleware1,Middleware2,中间件按声明顺序执行
middleware: AuthInterceptor
)
service shop-api {
@handler GetUserInfo
post /info (GetUserInfoReq) returns (GetUserInfoResp)
}
测试
Go
func (l *GetFocusListLogic) GetFocusList() (resp *types.FocusResp, err error) {
//延时3秒钟
time.Sleep(4 * time.Second)
// todo: add your logic here and delete this line
//返回一个模拟的轮播图列表数据
focusList := []*types.Focus{
{
Id: 1,
Name: "轮播图1",
Pic: "https://picsum.photos/200/300",
Position: 1,
Url: "https://www.baidu.com",
},
{
Id: 2,
Name: "轮播图2",
Pic: "https://picsum.photos/200/300",
Position: 2,
Url: "https://www.baidu.com",
},
}
return &types.FocusResp{
Result: focusList,
}, nil
}
3.6.1 @doc 语句
在 go-zero 的 .api 文件中,@doc 是一个用于为接口、结构体或字段添加描述性注释的语法块,它的作用是为生成的代码或文档提供说明信息,例如接口用途、参数含义、返回结果等,使 API 定义更加清晰易读。通过 @doc,开发人员可以在代码中直接编写文档,配合 goctl 工具自动生成接口文档(如 Markdown 或 Swagger),实现代码与文档的统一,方便团队协作和前后端联调。
@doc 语句是对单个路由的 meta 信息描述,一般为 key-value 值,可以传递给 goctl 及其插件来进行扩展生成。
Go
// 单行 @doc
@doc "foo"
// 有内容的 @doc 组
@doc (
foo: "bar"
bar: "baz"
)
Go
syntax = "v1"
// Focus的请求参数
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
// 文章的请求参数
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
//用户的请求参数
type (
GetUserInfoReq {
Id int64 `form:"id"` // 使用 form tag 表示 x-www-form-urlencoded 数据
}
GetUserInfoResp {
Id int64 `json:"id"`
Name string `json:"name"`
Desc string `json:"desc"`
}
)
//轮播图的服务
@server (
group: focus
prefix: /api/focus
timeout: 3s
)
service shop-api {
@doc "获取一个轮播图"
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
@doc "获取轮播图列表"
@handler GetFocusList
get /list returns (FocusResp)
}
//文章的服务
@server (
group: article
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
//用户的服务
@server (
group: user
prefix: /api/user
// 定义一个鉴权控制的中间件,多个中间件以英文逗号,分割,如 Middleware1,Middleware2,中
间件按声明顺序执行
middleware: AuthInterceptor
)
service shop-api {
@handler GetUserInfo
post /info (GetUserInfoReq) returns (GetUserInfoResp)
}
server.AddRoutes(
[]rest.Route{
{
// 获取一个轮播图
Method: http.MethodGet,
Path: "/details/:id",
Handler: focus.GetOneFocusHandler(serverCtx),
},
{
// 获取轮播图列表
Method: http.MethodGet,
Path: "/list",
Handler: focus.GetFocusListHandler(serverCtx),
},
},
rest.WithPrefix("/api/focus"),
rest.WithTimeout(3000*time.Millisecond),
)
3.7 import 语句
import 语句是在 api 中引入其他 api 文件的语法块,其支持相对/绝对路径
3.7.1 基本语法
Go
// 单行 import
import "foo"
import "/path/to/file"
// import 组
import ()
import (
"bar"
"relative/to/file"
)
3.7.2 使用
新建api/focus.api
Go
syntax = "v1"
// Focus的请求参数
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
//轮播图的服务
@server (
group: focus
prefix: /api/focus
timeout: 3s
)
service shop-api {
@doc "获取一个轮播图"
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
@doc "获取轮播图列表"
@handler GetFocusList
get /list returns (FocusResp)
}
新建api/user.api
Go
syntax = "v1"
//用户的请求参数
type (
GetUserInfoReq {
Id int64 `form:"id"` // 使用 form tag 表示 x-www-form-urlencoded 数据
}
GetUserInfoResp {
Id int64 `json:"id"`
Name string `json:"name"`
Desc string `json:"desc"`
}
)
//用户的服务
@server (
group: user
prefix: /api/user
// 定义一个鉴权控制的中间件,多个中间件以英文逗号,分割,如 Middleware1,Middleware2,中间件按声明顺序执行
middleware: AuthInterceptor
)
service shop-api {
@handler GetUserInfo
post /info (GetUserInfoReq) returns (GetUserInfoResp)
}
shop.api
Go
syntax = "v1"
import "api/focus.api"
import "api/user.api"
// 文章的请求参数
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
//文章的服务
@server (
group: article
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
第四章:go-zero 原生数据库访问方案
go-zero连接MySQL数据库教程官网案例: MySQL
4.1 go-zero 中绑定 PostgreSQL 数据库
创建 database_case 文件夹并进入文件夹中。
Go
# 创建文件夹
mkdir database_case;
# 进入文件夹中:
cd database_case;
4.1.1 项目创建
创建 shop.api 文件
Go
goctl api -o shop.api
将如下的代码添加到shop.api中。
Go
syntax = "v1"
// 返回值
type CommonResponse {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"` //omitempty表示Data可以返回也可以不返回
Success bool `json:"success"`
}
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
@server (
group: focus // 代表当前 service 代码块下的路由生成代码时都会被放到 focus 目录下
prefix: /api/focus
)
service shop-api {
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
@handler GetFocusList
get /list returns (FocusResp)
}
@server (
group: article // 代表当前 service 代码块下的路由生成代码时都会被放到 article 目录下
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
执行命令,生成相关项目文件。
Go
goctl api go --api shop.api --dir .;
下载项目相关依赖:
Go
go mod tidy;
上述内容完成项目的创建。
4.1.2 集成 PostgreSQL 数据库
go-zero 通过其内置的 sqlx 工具包(基于标准库 database/sql)以及可选的 GORM 集成,能够支持所有实现了 Go 驱动的关系型数据库,包括 MySQL、PostgreSQL、SQLite、SQL Server、TiDB、OceanBase 等。同时,通过 goctl 的 model 命令,可以针对不同数据库(如 MySQL 用 ddl 方式,PostgreSQL 用 datasource 方式)快速生成数据访问层代码,实现了对主流数据库的深度适配。对于 NoSQL 数据库如 Redis、MongoDB 等,go-zero 也提供了相应的客户端封装和中间件支持,因此可以灵活构建多数据源的应用。
1、新建数据库文件
在项目根目录中新建 model/pgsql 目录,并在其中新建 focus.sql 文件:
Go
your-project-name/
├── model/
│ └── pgsql/
│ └── focus.sql
model/pgsql/focus.sql 内容(PostgreSQL 语法):
Go
-- 创建表
CREATE TABLE focus (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NULL DEFAULT NULL,
pic VARCHAR(500) NULL DEFAULT NULL,
url VARCHAR(500) NULL DEFAULT NULL,
position BIGINT NULL DEFAULT 0,
created_at TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP
);
-- 创建索引
CREATE INDEX idx_position ON focus(position);
CREATE INDEX idx_created_at ON focus(created_at);
-- 添加注释
COMMENT ON TABLE focus IS '焦点图表';
COMMENT ON COLUMN focus.id IS '主键ID';
COMMENT ON COLUMN focus.title IS '名称';
COMMENT ON COLUMN focus.pic IS '图片地址';
COMMENT ON COLUMN focus.url IS '链接地址';
COMMENT ON COLUMN focus.position IS '排序位置';
COMMENT ON COLUMN focus.created_at IS '创建时间';
COMMENT ON COLUMN focus.updated_at IS '更新时间';
-- 可选:触发器实现 updated_at 自动更新
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_focus_updated_at
BEFORE UPDATE ON focus
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
在数据库中创建**Schema:go_zero** ,并在**Schema**中执行上述的创表语句,最后向表中插入数据。用于后续的验证。
Go
-- 插入测试数据(id 由 BIGSERIAL 自动生成)
INSERT INTO focus (title, pic, url, position) VALUES
('焦点图1', 'https://picsum.photos/id/1/800/400', 'https://example.com/1', 1),
('焦点图2', 'https://picsum.photos/id/2/800/400', 'https://example.com/2', 2),
('焦点图3', 'https://picsum.photos/id/3/800/400', 'https://example.com/3', 3),
('焦点图4', 'https://picsum.photos/id/4/800/400', 'https://example.com/4', 4),
('焦点图5', 'https://picsum.photos/id/5/800/400', 'https://example.com/5', 5);
2、生成 Model 代码
生成 Model 代码的作用是将数据库表结构自动映射为 Go 语言的结构体,并封装了常用的 CRUD(增、删、改、查)操作方法,让开发者无需手写繁琐的 SQL 语句,直接通过调用这些自动生成的方法就能完成数据访问,从而提高开发效率、减少手写错误,并确保数据库操作的一致性和类型安全。简单来说,它为你自动生成了"数据库操作工具箱",让你可以像操作普通对象一样操作数据表。
进入项目根目录,使用 goctl 的 PostgreSQL 数据源方式生成代码:
Go
# 进入项目目录
goctl model pg datasource --url "postgres://用户名:密码@localhost:5432/数据库名?sslmode=disable&search_path=go_zero" --schema go_zero --table focus --dir ./model/pgsql
根据实际的数据库内容,填写相关参数后执行下述命令。
Go
goctl model pg datasource --url "postgres://postgres:123456@localhost:5432/postgres?sslmode=disable" --schema go_zero --table focus --dir ./model/pgsql
执行后会在 model/pgsql 目录下生成 focusmodel.go、focusmodel_gen.go、vars.go 等文件。
注意 :确保数据库中存在
focus表,或者先手动执行上面的建表 SQL。
该命令用于基于 PostgreSQL 数据库中的现有表生成数据模型代码,各参数含义如下:
-
goctl**:**go-zero 框架的命令行工具,用于生成代码、管理项目等。 -
model**:**子命令,表示生成数据模型相关的代码。 -
pg:指定数据库类型为 PostgreSQL,告诉 goctl 使用 PostgreSQL 的驱动和语法生成模型。 -
datasource:子命令,表示从数据库数据源(即已存在的表)生成模型代码。 -
--url:指定 PostgreSQL 数据库的连接字符串,格式为postgres://用户名:密码@主机:端口/数据库名?参数。例如示例中的连接串包含了用户名、密码、主机、端口、数据库名以及sslmode=disable(禁用 SSL)等连接参数。 -
**
--schema:**指定表所在的schema名称 -
--table:指定要生成模型的表名,这里为focus,goctl 会读取该表的结构并生成对应的 Go 结构体和 CRUD 方法。 -
--dir:指定生成代码的输出目录,./model/pgsql表示在当前项目的model/pgsql目录下生成模型文件(如focusmodel.go、focusmodel_gen.go、vars.go等)。
3、下载依赖
自动下载并管理项目所需的数据库驱动和 go-zero 框架相关的依赖。具体包括:
-
数据库驱动: 例如 PostgreSQL 驱动(如
github.com/lib/pq或github.com/jackc/pgx),因为项目中使用了sqlx.NewPostgres或通过goctl生成的模型代码依赖了相应的驱动。 -
go-zero 核心存储包: 如
github.com/zeromicro/go-zero/core/stores/sqlx,用于数据库连接和操作。 -
**其他间接依赖:**根据导入的包自动解析并下载所有必需的第三方库,确保项目编译运行所需的环境完整。
Go
go mod tidy
4、配置数据库连接
根据你的数据库环境,etc/shop-api.yaml 配置文件可以按以下方式填写:
Go
Name: shop-api # 服务名称,用于日志和链路追踪标识
Host: 0.0.0.0 # 服务监听地址,0.0.0.0 表示监听所有网络接口
Port: 8888 # 服务监听端口
# PostgreSQL 数据库连接配置
# 格式:postgres://用户名:密码@主机:端口/数据库名?参数
DataSource: "postgres://postgres:123456@localhost:5432/postgres?sslmode=disable&search_path=go_zero"
参数详解:
-
postgres://:固定前缀,表示使用 PostgreSQL 协议连接。 -
postgres:123456:数据库用户名和密码,替换为实际值。 -
localhost:5432:数据库主机地址和端口,默认端口 5432。 -
postgres:要连接的数据库名(dbname),请根据你的实际数据库名修改,例如go_zero或postgres。 -
sslmode=disable:禁用 SSL 加密,本地开发常用。生产环境建议启用 SSL 并设置为require或verify-full。 -
search_path=go_zero:指定 PostgreSQL 的默认模式(schema),这样在代码中操作表时无需加模式前缀(例如直接使用focus而非go_zero.focus)。如果表位于其他模式(如public),可删除该参数或修改为对应的模式名。
注意:
-
如果表位于非
public模式,务必在连接字符串中通过search_path指定,或在代码中为每个表添加模式前缀。 -
确保数据库用户具有该模式表的访问权限。
-
连接字符串中的
sslmode为小写,请勿写成sslMode。
5、修改配置结构体
修改配置结构体的作用是将配置文件(etc/shop-api.yaml)中的数据库连接字符串(如 DataSource: "postgres://...")映射到 Go 结构体的字段上,使得程序启动时可以自动加载并注入该配置值,供后续服务上下文(如 ServiceContext)通过 c.DataSource 获取并使用,从而实现配置与代码的解耦,便于不同环境灵活切换数据库连接信息。
Go
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
// PostgreSQL 连接字符串
DataSource string
}
6、创建服务上下文并注入模型
编辑 internal/svc/servicecontext.go,使用 sqlx.NewPostgres 连接 PostgreSQL,并初始化 FocusModel:
Go
package svc
import (
"database_case/internal/config"
"database_case/model/pgsql"
"github.com/zeromicro/go-zero/core/stores/sqlx"
_ "github.com/lib/pq"
)
type ServiceContext struct {
Config config.Config
FocusModel pgsql.FocusModel
}
func NewServiceContext(c config.Config) *ServiceContext { // 注意这里返回类型大写 S
conn := sqlx.NewSqlConn("postgres", c.DataSource)
return &ServiceContext{ // 这里也是大写 S
Config: c,
FocusModel: pgsql.NewFocusModel(conn),
}
}
这一步的作用是在服务上下文(ServiceContext)中建立与 PostgreSQL 数据库的连接(通过 sqlx.NewPostgres 读取配置文件中的 DataSource),并将生成的 FocusModel 实例注入到上下文中,使得后续业务逻辑层(如 logic)可以直接通过 svcCtx.FocusModel 调用数据库操作方法,实现了依赖的统一管理和注入,避免了在每个逻辑层重复创建连接,确保了代码的整洁性和可维护性。
7、在逻辑层使用模型查询数据
以查询单条数据为例,编辑 internal/logic/focus/getonefocuslogic.go:
Go
package focus
import (
"context"
"database_case/internal/svc"
"database_case/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetOneFocusLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetOneFocusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOneFocusLogic {
return &GetOneFocusLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetOneFocusLogic) GetOneFocus(req *types.OneFocusReq) (resp *types.OneFocusResp, err error) {
// 查询数据
focus, err := l.svcCtx.FocusModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, err
}
return &types.OneFocusResp{
Result: types.Focus{
Id: focus.Id,
Name: focus.Title,
Pic: focus.Pic,
Position: focus.Position,
Url: focus.Url,
},
}, nil
}
注意:在上述过程中中出现报错,可能需要下载数据库驱动,可直接执行下述命令:
Go
# 下载驱动
go get github.com/lib/pq
# 讲包添加进依赖中
go mod tidy
运行程序:
Go
fresh
输入如下网址,查看返回数据:
Go
http://localhost:8888/api/focus/details/1
输出如下内容,即表示成功。

MySQL与PostgreSQL数据库的关键差异总结
| 项目 | MySQL 方式 | PostgreSQL 方式 |
|---|---|---|
| 建表语句 | AUTO_INCREMENT, ON UPDATE 等 |
BIGSERIAL, 触发器实现更新时间 |
| 连接字符串 | root:pass@tcp(...)/db?charset=... |
postgres://user:pass@host:port/db?sslmode=... |
| SQL 驱动 | sqlx.NewMysql |
sqlx.NewPostgres |
| 生成模型命令 | goctl model mysql ddl --src ... |
goctl model pg datasource --url ... --table ... |
| 占位符 | ? |
$1, $2(由驱动自动处理) |
通过以上步骤,您即可在 go-zero 项目中无缝集成 PostgreSQL 数据库。
要将教程中的 MySQL 实现改为 PostgreSQL,核心差异在于:
(1)Model 生成命令 :使用 goctl model pg datasource 替代 goctl model mysql ddl。
(2)数据库驱动 :导入 _ "``github.com/lib/pq``" 并使用 sqlx.NewSqlConn("postgres", c.DataSource)。
(3)Model 代码中的类型 :PostgreSQL 中允许 NULL 的字段会生成 sql.NullString 等类型,需在业务层正确处理。
4.2 查询多条数据
4.2.1 修改 Model 层
在 model/pgsql/focusmodel_gen.go 的接口和实现中添加 FindAll 方法。
修改接口 focusModel:
Go
// focusModel 定义了对数据库表 focus 进行数据访问的接口,封装了常用的 CRUD 操作。
type focusModel interface {
// Insert 向数据库表中插入一条记录。
// 参数:
// ctx: 上下文,用于传递请求范围的值(如超时、取消信号)。
// data: 指向要插入的 Focus 结构体的指针,包含要插入的字段值。
// 返回值:
// sql.Result: 执行结果,可获取插入的自增 ID(如果表有自增主键)。
// error: 插入过程中发生的错误(如数据库连接失败、字段不匹配等)。
Insert(ctx context.Context, data *Focus) (sql.Result, error)
// FindOne 根据主键 id 查询单条记录。
// 参数:
// ctx: 上下文。
// id: 要查询的记录的主键值。
// 返回值:
// *Focus: 查询到的记录对应的结构体指针,如果记录不存在则返回 nil。
// error: 查询过程中发生的错误(如数据库错误)。
FindOne(ctx context.Context, id int64) (*Focus, error)
// FindAll 查询表中所有记录。
// 参数:
// ctx: 上下文。
// 返回值:
// []*Focus: 所有记录的结构体指针切片,如果表为空则返回空切片。
// error: 查询过程中发生的错误。
FindAll(ctx context.Context) ([]*Focus, error)
// Update 更新数据库表中的一条记录。
// 注意:更新时通常依赖主键字段进行定位,因此 data 中应包含有效的主键值。
// 参数:
// ctx: 上下文。
// data: 指向要更新的 Focus 结构体的指针,需要包含主键以及要更新的字段值。
// 返回值:
// error: 更新过程中发生的错误。
Update(ctx context.Context, data *Focus) error
// Delete 根据主键 id 删除一条记录。
// 参数:
// ctx: 上下文。
// id: 要删除的记录的主键值。
// 返回值:
// error: 删除过程中发生的错误。
Delete(ctx context.Context, id int64) error
}
实现 FindAll 方法 (注意使用 PostgreSQL 占位符 $1 等,但这里无参数,直接查询):
Go
// FindAll 查询 focus 表中的所有记录。
// 该方法通过拼接 SQL 语句获取表中所有列,并使用 QueryRowsCtx 将结果集映射到 Focus 切片。
// 参数:
// ctx: 上下文,用于控制超时或传递请求范围的值。
// 返回值:
// []*Focus: 包含所有记录的结构体指针切片,如果表为空则返回空切片。
// error: 查询过程中发生的任何错误(如数据库连接失败、SQL 语法错误等)。
func (m *defaultFocusModel) FindAll(ctx context.Context) ([]*Focus, error) {
// 构建查询语句:select 列名列表 from 表名
// focusRows 是在文件开头定义的常量,包含表中所有需要查询的字段名(以逗号分隔)
// m.table 是模型对应的表名
query := fmt.Sprintf("select %s from %s", focusRows, m.table)
// 定义切片用于存储查询结果
var resp []*Focus
// 执行查询,将结果映射到 resp 切片中
// QueryRowsCtx 是 go-zero 提供的封装方法,会自动处理占位符、连接池和结果扫描
err := m.conn.QueryRowsCtx(ctx, &resp, query)
// 返回结果和可能的错误
return resp, err
}
4.2.2 修改业务逻辑层
在(internal/logic/focus/getfocuslistlogic.go)文件中添加如下代码:
Go
func (l *GetFocusListLogic) GetFocusList() (resp *types.FocusResp, err error) {
// 查询所有数据
list, err := l.svcCtx.FocusModel.FindAll(l.ctx)
if err != nil {
return nil, err
}
var focusList []*types.Focus
for _, focus := range list {
focusList = append(focusList, &types.Focus{
Id: focus.Id,
Name: focus.Title, // 直接使用 Title
Pic: focus.Pic, // 直接使用 Pic
Url: focus.Url, // 直接使用 Url
Position: focus.Position,
})
}
return &types.FocusResp{
Result: focusList,
}, nil
}
这段代码是 go-zero 框架中业务逻辑层(Logic)的一个方法,用于查询焦点图列表并返回给客户端。下面逐行解释其作用和整体流程。
Go
func (l *GetFocusListLogic) GetFocusList() (resp *types.FocusResp, err error) {
}
-
函数签名 :
GetFocusList是GetFocusListLogic结构体的方法,它接收一个接收者l(通常包含服务上下文svcCtx),没有请求参数,返回一个*types.FocusResp响应对象和一个错误。 -
作用 :该方法是处理 HTTP GET 请求
GET /api/focus/list的具体业务实现,由 go-zero 的 handler 层自动调用。
Go
// 查询所有数据
list, err := l.svcCtx.FocusModel.FindAll(l.ctx)
if err != nil {
return nil, err
}
-
l.svcCtx:服务上下文,在初始化时注入了数据库模型FocusModel。 -
FocusModel.FindAll:调用数据访问层(Model)的方法,执行SELECT * FROM focus查询,返回所有记录的切片list和可能的错误。 -
错误处理 :如果查询失败,直接返回
nil和错误,由上层 handler 转为 HTTP 错误响应。
Go
var focusList []*types.Focus
for _, focus := range list {
focusList = append(focusList, &types.Focus{
Id: focus.Id,
Name: focus.Title, // 直接使用 Title
Pic: focus.Pic, // 直接使用 Pic
Url: focus.Url, // 直接使用 Url
Position: focus.Position,
})
}
-
类型转换 :将数据模型层的
Focus结构体(可能包含数据库特定类型,如sql.NullString)转换为 API 层定义的types.Focus(纯 JSON 结构体)。 -
注意 :这里的
focus.Title、focus.Pic、focus.Url如果是sql.NullString类型,则需要用.String取值;此处假设它们已是普通string类型(或模型层已处理)。 -
切片构建 :将每个模型对象转换为 API 对象后,添加到
focusList切片中。
Go
return &types.FocusResp{
Result: focusList,
}, nil
}
- 返回响应 :构造
types.FocusResp结构体,其Result字段包含焦点图列表,并返回nil错误表示成功。
各组成部分作用总结
| 组成部分 | 作用 |
|---|---|
GetFocusListLogic |
业务逻辑结构体,封装与焦点图列表相关的业务方法。 |
l.svcCtx |
服务上下文,集中管理数据库连接、模型、配置等依赖。 |
FocusModel.FindAll |
数据访问层方法,执行数据库查询并返回原始数据。 |
types.Focus |
API 层定义的数据结构,用于 JSON 序列化返回给前端。 |
types.FocusResp |
API 层定义的统一响应结构,包含 Result 字段。 |
该函数遵循 go-zero 的分层架构:Handler → Logic → Model,逻辑层只负责调用模型获取数据并进行简单的数据转换,保持单一职责,便于测试和维护。
运行代码:
Go
fresh
查看网址:
Go
http://localhost:8888/api/focus/list
输出结果:

4.3 增加数据
在 go-zero 中增加数据(即插入新记录)的核心流程是:先在 .api 文件中用 form 标签定义接收前端表单数据的结构体(如 AddFocusReq),然后通过 goctl 生成对应的业务逻辑文件;在 logic 层的 AddXxx 方法中,将接收到的请求参数转换为数据模型(如 pgsql.Focus),并调用模型层的 Insert 方法将数据写入 PostgreSQL 数据库。若表中字段允许为空,模型字段会使用 sql.NullString 类型,此时需用 sql.NullString{String: req.Value, Valid: req.Value != ""} 构造;若字段为非空,则直接赋值普通字符串。最后根据插入结果返回统一的 CommonResponse 响应即可。
4.3.1 修改shop.api
修改shop.api,添加AddusResp部分内容共,用于添加数据。
Go
syntax = "v1"
type CommonResponse {
Message string `json:"message"`
Code int `json:"code"`
Success bool `json:"success"`
}
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
AddFocusReq {
Name string `form:"name"`
Pic string `form:"pic"`
Url string `form:"url"`
Position int64 `form:"position"`
}
UpdateFocusReq {
Id int64 `form:"id"`
Name string `form:"name"`
Pic string `form:"pic"`
Url string `form:"url"`
Position int64 `form:"position"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
@server (
group: focus // 代表当前 service 代码块下的路由生成代码时都会被放到 focus 目录下
prefix: /api/focus
)
service shop-api {
//焦点图详情
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
//轮播图列表
@handler GetFocusList
get /list returns (FocusResp)
//增加数据
@handler AddFocus
post /add (AddFocusReq) returns (CommonResponse)
//修改数据
@handler UpdateFocus
put /update (UpdateFocusReq) returns (CommonResponse)
//删除数据
@handler DeleteFocus
delete /delete/:id (OneFocusReq) returns (CommonResponse)
}
@server (
group: article // 代表当前 service 代码块下的路由生成代码时都会被放到 article 目录下
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
**注意:**获取表单数据需要配置请求体tag
Go
AddFocusReq {
Name string `form:"name"`
Pic string `form:"pic"`
Url string `form:"url"`
Position int64 `form:"position"`
}
4.3.2 生成项目
Go
goctl api go --api shop.api --dir .
4.3.3 修改业务逻辑
修改focus/addfocuslogic.go
Go
package focus
import (
"context"
// "database/sql" // 导入 database/sql 以使用 sql.NullString
"database_case/internal/svc"
"database_case/internal/types"
"database_case/model/pgsql" // 注意导入的是 pgsql 包,不是 mysql
"github.com/zeromicro/go-zero/core/logx"
)
type AddFocusLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAddFocusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddFocusLogic {
return &AddFocusLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AddFocusLogic) AddFocus(req *types.AddFocusReq) (resp *types.CommonResponse, err error) {
// 构造 PostgreSQL 模型对象,注意使用 pgsql.Focus
// 如果表中字段允许 NULL,则需要用 sql.NullString 包装
focus := pgsql.Focus{
Title: req.Name, // 直接使用 string
Pic: req.Pic,
Url: req.Url,
Position: req.Position,
}
// 执行插入操作
_, err = l.svcCtx.FocusModel.Insert(l.ctx, &focus)
if err != nil {
return nil, err
}
// 返回成功响应
return &types.CommonResponse{
Code: 0,
Message: "添加成功",
}, nil
}
这里需要注意类型转换
-
如果用户传入了空字符串,数据库会存储为NULL
-
如果用户传入了非空字符串,数据库会存储该字符串值
运行代码:
Go
fresh
通过postman访问如下地址:
Go
http://127.0.0.1:8888/api/focus/add

查看数据库中的数据是否完成增加:

4.4 修改数据
4.4.1 修改shop.api
shop.api和增加数据里面是一样的
Go
syntax = "v1"
type CommonResponse {
Message string `json:"message"`
Code int `json:"code"`
Success bool `json:"success"`
}
type (
OneFocusReq {
Id int64 `path:"id"` // 使用 path tag 表示路径参数
}
OneFocusResp {
Result Focus `json:"result"`
}
FocusResp {
Result []*Focus `json:"result"`
}
AddFocusReq {
Name string `form:"name"`
Pic string `form:"pic"`
Url string `form:"url"`
Position int64 `form:"position"`
}
UpdateFocusReq {
Id int64 `form:"id"`
Name string `form:"name"`
Pic string `form:"pic"`
Url string `form:"url"`
Position int64 `form:"position"`
}
Focus {
Id int64 `json:"id"`
Name string `json:"name"`
Pic string `json:"pic"`
Url string `json:"url"`
Position int64 `json:"position"`
}
)
type (
OneArticleReq {
Id int64 `form:"id"` // Get传值只保留form tag
}
OneArticleResp {
Result Article `json:"result"`
}
ArticleResp {
Result []*Article `json:"result"`
}
Article {
Id int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Pic string `json:"pic"`
Content string `json:"content"`
}
)
@server (
group: focus // 代表当前 service 代码块下的路由生成代码时都会被放到 focus 目录下
prefix: /api/focus
)
service shop-api {
//焦点图详情
@handler GetOneFocus
get /details/:id (OneFocusReq) returns (OneFocusResp)
//轮播图列表
@handler GetFocusList
get /list returns (FocusResp)
//增加数据
@handler AddFocus
post /add (AddFocusReq) returns (CommonResponse)
//修改数据
@handler UpdateFocus
put /update (UpdateFocusReq) returns (CommonResponse)
//删除数据
@handler DeleteFocus
delete /delete/:id (OneFocusReq) returns (CommonResponse)
}
@server (
group: article // 代表当前 service 代码块下的路由生成代码时都会被放到 article 目录下
prefix: /api/article
)
service shop-api {
@handler GetOneArticle
get /details (OneArticleReq) returns (OneArticleResp)
@handler GetArticleList
get /list returns (ArticleResp)
}
**注意:**获取表单数据需要配置请求体tag
Go
AddFocusReq {
Name string `form:"name"`
Pic string `form:"pic"`
Url string `form:"url"`
Position int64 `form:"position"`
}
4.4.2 生成项目
Go
goctl api go --api shop.api --dir .
4.4.3 修改业务逻辑
修改focus/updatefocuslogic.go
Go
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package focus
import (
"context"
"database_case/internal/svc"
"database_case/internal/types"
"database_case/model/pgsql" // 使用 pgsql 包
"github.com/zeromicro/go-zero/core/logx"
)
type UpdateFocusLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUpdateFocusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateFocusLogic {
return &UpdateFocusLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateFocusLogic) UpdateFocus(req *types.UpdateFocusReq) (resp *types.CommonResponse, err error) {
// 构造数据模型(假设模型中字段为普通 string,直接赋值)
focus := pgsql.Focus{
Id: req.Id,
Title: req.Name,
Pic: req.Pic,
Url: req.Url,
Position: req.Position,
}
// 执行更新
err = l.svcCtx.FocusModel.Update(l.ctx, &focus)
if err != nil {
return nil, err
}
// 返回成功响应
return &types.CommonResponse{
Code: 0,
Message: "修改成功",
}, nil
}
这里需要注意类型转换
-
如果用户传入了空字符串,数据库会存储为NULL
-
如果用户传入了非空字符串,数据库会存储该字符串值
(1)运行项目:
Go
fresh
(2)通过postman访问网址,完成数据更新:
Go
http://127.0.0.1:8888/api/focus/update
(3)输出如下内容:

(4) 验证数据

4.5 删除数据
4.5.1 修改业务逻辑
将下述的内容添加进E:\web3_study\golang_study\database_case\ internal\logic\focus\deletefocuslogic.go 文件中。
Go
func (l *DeleteFocusLogic) DeleteFocus(req *types.OneFocusReq) (resp *types.CommonResponse, err error) {
// 删除数据
err = l.svcCtx.FocusModel.Delete(l.ctx, req.Id)
if err != nil {
return &types.CommonResponse{
Code: 500,
Message: "删除失败",
Success: false,
}, nil
}
return &types.CommonResponse{
Code: 200,
Message: "删除成功",
Success: true,
}, nil
}
(1)运行项目
Go
fresh
(2)通过postman访问如下网址
Go
# 注意:链接后面的即为三删除 id = 1的数据。
http://127.0.0.1:8888/api/focus/delete/1
(3)输出如下内容
注意

(4)验证数据
数据库中显示,id = 1的即被删除。

第五章:go-zero 中原生 SQL 支持
5.1 引言
在使用 go-zero 开发后端服务时,我们通常使用 goctl model 自动生成数据访问层(Model)代码,这已经覆盖了绝大多数单表 CRUD 操作。然而,在实际企业级应用中,经常会遇到复杂查询、多表关联、性能优化或特定数据库功能(如 PostgreSQL 的 JSON 操作)等场景,这时自动生成的 Model 方法可能无法满足需求。因此,掌握如何在 go-zero 中执行原生 SQL 是每个开发者的必备技能。
5.2 为什么需要原生 SQL?
自动生成的 Model 代码已经提供了 Insert、FindOne、Update、Delete 等常用方法,但在以下场景中,我们需要编写原生 SQL:
(1)复杂多表关联查询:例如在 DEX 交易所后台需要查询订单详情,同时关联用户表和币种表。
(2)聚合统计:如统计每个 NFT 系列的总交易量、平均价格。
(3)特定数据库功能 :PostgreSQL 的 JSONB 查询、全文检索、数组操作等。
(4)批量操作优化 :一次插入上千条数据,或使用 UPSERT 语法。
(5) 动态条件查询:前端传入了多个可选筛选条件,需要动态拼接 WHERE 子句。
go-zero 内置的 sqlx 包(github.com/zeromicro/go-zero/core/stores/sqlx)提供了强大且安全的原生 SQL 执行能力,支持参数化查询、事务、连接池管理等,与框架完美融合。
5.3 执行原生 SQL 的环境准备
5.3.1 创建数据库连接
在使用原生 SQL 之前,你需要先建立数据库连接。go-zero 的 sqlx 提供了统一的连接创建方式。
示例:在 servicecontext.go 中初始化连接.
Go
// internal/svc/servicecontext.go
package svc
import (
"your-module/internal/config"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
Conn sqlx.SqlConn // 原生 SQL 执行器
}
func NewServiceContext(c config.Config) *ServiceContext {
// 创建数据库连接,第一个参数为驱动名(postgres/mysql),第二个为数据源字符串
conn := sqlx.NewSqlConn("postgres", c.DataSource)
return &ServiceContext{
Config: c,
Conn: conn,
}
}
说明:
-
sqlx.NewSqlConn返回一个实现了sqlx.SqlConn接口的对象,该对象提供了执行原生 SQL 的所有方法。 -
驱动名可以是
"postgres"、"mysql"等,需与使用的数据库类型匹配。
5.3.2 配置数据库驱动
在 go.mod 中需要引入对应的数据库驱动。对于 PostgreSQL,使用 github.com/lib/pq:
Go
go get github.com/lib/pq
并在某个文件中(如 main.go)空白导入以注册驱动:
Go
import _ "github.com/lib/pq"
5.4 原生 SQL 的基本用法
sqlx.SqlConn 提供了三类核心方法:Exec(执行 DML/DDL)、QueryRow(查询单行)、QueryRows(查询多行)。所有方法都支持参数化查询,避免 SQL 注入。
5.4.1 执行 DML(插入、更新、删除)
方法签名 :Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
示例:插入一条焦点图记录
Go
func (l *AddFocusLogic) AddFocus(req *types.AddFocusReq) (*types.CommonResponse, error) {
// 构建 SQL 语句,使用 $1, $2... 作为占位符(PostgreSQL 风格)
query := `INSERT INTO focus (title, pic, url, position) VALUES ($1, $2, $3, $4)`
result, err := l.svcCtx.Conn.Exec(l.ctx, query, req.Title, req.Pic, req.Url, req.Position)
if err != nil {
return nil, err
}
rowsAffected, _ := result.RowsAffected()
// 获取插入后生成的自增 ID(如果需要)
lastId, _ := result.LastInsertId()
return biz.Success(map[string]interface{}{
"id": lastId,
"rows": rowsAffected,
}), nil
}
关键点:
-
使用占位符
$1、$2...(PostgreSQL)或?(MySQL),严禁使用字符串拼接。 -
Exec返回sql.Result,可以获取影响行数和最后插入 ID(依赖驱动支持)。
5.4.2 查询单行数据
方法签名 :QueryRow(ctx context.Context, dest interface{}, query string, args ...interface{}) error
示例:根据 ID 查询焦点图详情
Go
type Focus struct {
Id int64
Title string
Pic string
Url string
Position int64
}
func (l *GetOneFocusLogic) GetOneFocus(req *types.OneFocusReq) (*types.OneFocusResp, error) {
var focus Focus
query := `SELECT id, title, pic, url, position FROM focus WHERE id = $1`
err := l.svcCtx.Conn.QueryRow(l.ctx, &focus, query, req.Id)
if err != nil {
if err == sql.ErrNoRows {
return nil, biz.DataNotExist
}
return nil, err
}
return &types.OneFocusResp{Result: focus}, nil
}
说明:
-
dest必须是一个指向结构体的指针,结构体字段名(不区分大小写)会自动与查询结果的列名匹配。 -
如果查询结果为空,返回
sql.ErrNoRows错误。
5.4.3 查询多行数据
方法签名 :QueryRows(ctx context.Context, dest interface{}, query string, args ...interface{}) error
示例:查询所有焦点图
Go
func (l *GetFocusListLogic) GetFocusList() (*types.FocusResp, error) {
var focusList []Focus
query := `SELECT id, title, pic, url, position FROM focus ORDER BY position`
err := l.svcCtx.Conn.QueryRows(l.ctx, &focusList, query)
if err != nil {
return nil, err
}
return &types.FocusResp{Result: focusList}, nil
}
注意:
-
dest必须是切片的指针,如&[]Focus。 -
查询结果会自动填充到切片中,即使结果为空也不会报错(切片长度为 0)。
3.4 高级:使用 MapScan 灵活处理结果
当查询结果集结构不固定(例如动态列)时,可以使用 QueryRowPartial 或 QueryRowsPartial,配合 MapScan 将结果映射到 map[string]interface{}。
示例:查询任意表的前 10 条记录
Go
func (l *DynamicQueryLogic) GetAnyTable(tableName string) ([]map[string]interface{}, error) {
query := fmt.Sprintf("SELECT * FROM %s LIMIT 10", tableName) // 注意:表名不能参数化,需谨慎
var results []map[string]interface{}
err := l.svcCtx.Conn.QueryRowsPartial(l.ctx, &results, query)
return results, err
}
说明:
-
QueryRowsPartial会将每一行数据填充到map[string]interface{}中,键为列名。 -
这种方式牺牲了类型安全,但在构建通用查询工具时非常有用。
5.5 事务处理
在涉及多表操作或需要保证原子性的场景中,必须使用事务。go-zero 的 sqlx 提供了简单的事务 API。
5.5.1 基本事务用法
方法 :TransactCtx(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error
示例:创建订单时扣减库存
Go
func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderReq) (*types.CommonResponse, error) {
err := l.svcCtx.Conn.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 1. 扣减库存(使用 session 执行 SQL)
_, err := session.Exec(ctx, "UPDATE product SET stock = stock - 1 WHERE id = $1 AND stock > 0", req.ProductId)
if err != nil {
return err
}
// 2. 创建订单
_, err = session.Exec(ctx, "INSERT INTO orders (user_id, product_id, quantity) VALUES ($1, $2, $3)",
req.UserId, req.ProductId, 1)
return err
})
if err != nil {
return nil, err
}
return biz.Success("订单创建成功"), nil
}
关键点:
-
在
TransactCtx闭包内,所有数据库操作必须使用传入的session对象(而不是l.svcCtx.Conn),这样才能保证它们处于同一事务中。 -
如果闭包返回错误,事务自动回滚;返回
nil则自动提交。 -
事务支持嵌套(savepoint),但较少使用。
5.5.2 手动控制事务
如果需要更精细的控制(如根据条件决定提交或回滚),也可以使用 Begin、Commit、Rollback 方法,但需要自行管理事务对象的生命周期。通常推荐使用 TransactCtx。
5.6 高级特性
5.6.1 预处理语句(Prepared Statement)
对于频繁执行的 SQL,使用预处理语句可以提高性能。sqlx.SqlConn 提供了 Prepare 方法。
示例:批量插入
Go
func (l *BatchInsertLogic) BatchInsert(items []*types.Item) error {
stmt, err := l.svcCtx.Conn.Prepare(`INSERT INTO items (name, price) VALUES ($1, $2)`)
if err != nil {
return err
}
defer stmt.Close()
for _, item := range items {
_, err := stmt.Exec(l.ctx, item.Name, item.Price)
if err != nil {
return err
}
}
return nil
}
5.6.2 命名参数
虽然 sqlx 默认使用位置占位符,但也可以通过自定义扩展支持命名参数。go-zero 内部未直接提供命名参数,但可以借助第三方库(如 sqlx 的 NamedExec),不过这会引入额外依赖。通常建议使用位置占位符,简单清晰。
5.6.3 动态 SQL 构建
当查询条件不确定时,可以使用 strings.Builder 或 fmt.Sprintf 动态构建 SQL,但必须确保所有用户输入都通过占位符传递,而非直接拼接。
示例:根据可选条件查询
Go
func (l *SearchFocusLogic) SearchFocus(req *types.SearchReq) ([]*Focus, error) {
query := "SELECT * FROM focus WHERE 1=1"
var args []interface{}
if req.Title != "" {
query += " AND title ILIKE $1"
args = append(args, "%"+req.Title+"%")
}
if req.Position > 0 {
query += " AND position = $2"
args = append(args, req.Position)
}
// 注意:占位符编号要随着参数增加而变化,这里用 $1、$2 等,但简单拼接时最好使用 $1、$2 这样,或者在构建时动态生成占位符
// 更安全的方式是使用 sqlx 的 NamedExec,但需要调整。
// 这里演示通过拼接字符串简单示例,实际项目中建议使用结构化方式。
var results []*Focus
err := l.svcCtx.Conn.QueryRows(l.ctx, &results, query, args...)
return results, err
}
最佳实践 :对于复杂的动态查询,建议使用 sqlx 的 In 扩展或考虑使用 go-zero 的 sqlc 插件生成代码。
5.7 安全与性能注意事项
5.7.1 防止 SQL 注入
-
永远不要使用字符串拼接将用户输入嵌入 SQL 语句。
-
始终使用
$1、$2(PostgreSQL)或?(MySQL)占位符。 -
表名、列名无法参数化,如果需要动态表名,必须进行白名单校验。
危险示例(禁止):
Go
query := "SELECT * FROM " + tableName + " WHERE id = " + id // 绝对禁止!
安全示例:
Go
query := "SELECT * FROM focus WHERE id = $1"
5.7.2 连接池配置
sqlx.NewSqlConn 默认使用合理的连接池参数(最大空闲连接数、最大打开连接数等)。你可以通过 sqlx.WithMaxIdleConns、sqlx.WithMaxOpenConns 等选项自定义:
Go
conn := sqlx.NewSqlConn("postgres", c.DataSource,
sqlx.WithMaxIdleConns(10),
sqlx.WithMaxOpenConns(100),
)
5.7.3 日志与监控
go-zero 的 sqlx 在执行 SQL 时会自动打印慢查询日志(默认超过 200ms 会记录)。可以在配置文件中调整:
Go
# etc/app.yaml
Log:
Level: info
SlowThreshold: 100 # 毫秒
同时,可以通过 sqlx.WithLogger 注入自定义日志记录器。
5.8 实战案例:Web3.0 NFT 市场后台
5.8.1 场景描述
我们需要实现一个接口,统计每个 NFT 系列(collection)的地板价、交易量、持有人数等指标。表结构如下:
-
nft_items:单个 NFT 信息(id, collection_id, token_id, owner, price) -
collections:系列信息(id, name, creator, supply) -
transactions:交易记录(id, collection_id, token_id, price, buyer, seller, timestamp)
由于涉及多表关联和聚合,自动生成的 Model 无法直接完成,需要使用原生 SQL。
5.8.2 实现步骤
1. 定义返回结构
Go
type CollectionStats struct {
Id int64 `db:"id"`
Name string `db:"name"`
FloorPrice float64 `db:"floor_price"`
TotalVolume float64 `db:"total_volume"`
OwnersCount int `db:"owners_count"`
}
2. 编写原生 SQL
Go
SELECT
c.id,
c.name,
MIN(i.price) AS floor_price,
COALESCE(SUM(t.price), 0) AS total_volume,
COUNT(DISTINCT i.owner) AS owners_count
FROM collections c
LEFT JOIN nft_items i ON c.id = i.collection_id
LEFT JOIN transactions t ON i.id = t.nft_item_id
WHERE c.id = $1
GROUP BY c.id, c.name
3. 在 logic 中执行
Go
func (l *GetCollectionStatsLogic) GetCollectionStats(req *types.StatsReq) (*types.StatsResp, error) {
query := `
SELECT c.id, c.name,
MIN(i.price) AS floor_price,
COALESCE(SUM(t.price), 0) AS total_volume,
COUNT(DISTINCT i.owner) AS owners_count
FROM collections c
LEFT JOIN nft_items i ON c.id = i.collection_id
LEFT JOIN transactions t ON i.id = t.nft_item_id
WHERE c.id = $1
GROUP BY c.id, c.name
`
var stats CollectionStats
err := l.svcCtx.Conn.QueryRow(l.ctx, &stats, query, req.CollectionId)
if err != nil {
if err == sql.ErrNoRows {
return nil, biz.DataNotExist
}
return nil, err
}
return &types.StatsResp{Data: stats}, nil
}
4. 注意列名映射
SQL 查询结果的列名(如 floor_price)需要与结构体字段的 db 标签匹配,或者结构体字段名与列名相同(不区分大小写)。这里我们使用了 db 标签明确指定。
5.8.3 性能优化建议
-
为常用查询字段(如
collection_id)添加索引。 -
对于聚合统计,可考虑使用物化视图或定时任务提前计算,避免实时计算压力。
-
使用
EXPLAIN分析 SQL 执行计划。
5.9 与 GORM 的比较及选择
虽然 GORM 也是一个流行的 Go ORM,但在 go-zero 生态中,官方推荐使用 sqlx 作为数据库访问基础。原因如下:
| 特性 | go-zero sqlx |
GORM |
|---|---|---|
| 性能 | 较高,接近原生 SQL | 有轻微反射开销 |
| 学习成本 | 需要编写 SQL | 更易上手,链式操作 |
| 灵活性 | 完全掌控 SQL | 适合简单场景,复杂查询需回退到原生 SQL |
| 与 go-zero 集成 | 无缝集成,支持 goctl 生成 | 需手动集成,无法利用 goctl model 生成 |
| 事务 | 支持 | 支持 |
| 代码生成 | goctl model 生成模型 |
手动定义模型 |
结论 :在 go-zero 项目中,对于常规 CRUD,优先使用 goctl model 生成的代码;对于复杂查询和性能关键场景,使用原生 SQL(通过 sqlx.SqlConn)是更优选择。两者可以混合使用,互不冲突。
第六章:go-zero 中 JWT 集成
6.1 JWT 在企业应用中的定位
JWT 是一种无状态认证机制,广泛应用于微服务架构、前后端分离项目以及分布式系统中。其优势在于:
-
无需服务端存储会话,减轻数据库压力,天然支持水平扩展。
-
自包含,用户信息可直接存储在 token 中,减少额外查询。
-
跨域友好,可轻松在多个服务间传递。
但需要注意,JWT 一旦签发便无法主动撤销,因此需配合短过期时间 + Refresh Token 或黑名单机制来应对安全风险。
JSON Web Token(JWT)是一个轻量级的认证规范,这个规范允许使用JWT在用户和服务器之间传递安全可靠的信息,用于验证和授权用户访问资源。其本质是一个JSON 形式的token,包含了一些权利声明和所提供的信息,通常在用户成功进行身份验证后发放。在使用 JWT 进行身份验证时,客户端(如浏览器)会将令牌作为身份凭证发送到服务器端,服务器端会验证该令牌是否有效,并检查其中包含的权限和身份信息以授权用户对资源的访问。
6.2 项目结构设计(推荐)
Go
jwt-demo/
├── api/ # API 描述文件
│ ├── user.api
│ └── address.api
├── jwt.api # 主入口 API
├── internal/
│ ├── biz/ # 业务通用层
│ │ ├── code.go # 业务状态码定义
│ │ ├── error.go # 自定义错误类型
│ │ ├── response.go # 统一响应格式
│ │ └── jwt.go # JWT 生成工具
│ ├── config/ # 配置结构
│ ├── handler/ # 请求处理层(自动生成)
│ ├── logic/ # 业务逻辑层(自动生成)
│ ├── svc/ # 服务上下文
│ └── types/ # 请求/响应结构(自动生成)
├── etc/ # 配置文件
│ └── user-api.yaml
├── go.mod
└── main.go
6.3 API 定义
6.3.1 登录接口 api/user.api
Go
syntax = "v1"
type (
LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
LoginResp {
Token string `json:"token"`
}
)
@server (
group: account
prefix: "/api"
)
service user-api {
@handler login
post /user/login (LoginReq) returns (CommonResponse)
}
6.3.2 受保护接口 api/address.api
Go
syntax = "v1"
type Address {
Id int64 `json:"id"`
Address string `json:"address"`
Phone string `json:"phone"`
Name string `json:"name"`
}
@server (
group: address
prefix: "/api"
)
service user-api {
@handler address
get /address/list () returns (CommonResponse)
}
6.3.3 主入口 jwt.api
Go
syntax = "v1"
type CommonResponse {
Message string `json:"message"`
Code int `json:"code"`
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
}
import "api/user.api"
import "api/address.api"
6.4 代码生成与依赖管理
Go
goctl api go --api jwt.api --dir .
go mod tidy
6.5 统一业务处理封装(internal/biz)
6.5.1 业务状态码 code.go
Go
package biz
const Ok = 200
var (
DBError = NewError(10000, "数据库错误")
ParamError = NewError(10001, "参数错误")
DataNotExist = NewError(10002, "数据不存在")
ServerError = NewError(10003, "服务器错误")
UserNotExist = NewError(10004, "用户不存在")
UserNotLogin = NewError(10005, "用户未登录")
UserNotExistOrPasswordError = NewError(10006, "用户不存在或密码错误")
)
6.5.2 自定义错误类型 error.go
Go
package biz
type ErrorUtil struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (e *ErrorUtil) Error() string {
return e.Msg
}
func NewError(code int, msg string) *ErrorUtil {
return &ErrorUtil{Code: code, Msg: msg}
}
6.5.3 统一响应 response.go
Go
package biz
import "your-module/internal/types"
func Success(data any) *types.CommonResponse {
return &types.CommonResponse{
Code: Ok,
Message: "success",
Success: true,
Data: data,
}
}
func Error(err *ErrorUtil) *types.CommonResponse {
return &types.CommonResponse{
Code: err.Code,
Message: err.Msg,
Success: false,
}
}
6.6 全局错误处理(main.go)
Go
package main
import (
"flag"
"fmt"
"net/http"
"your-module/internal/biz"
"your-module/internal/config"
"your-module/internal/handler"
"your-module/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
)
var configFile = flag.String("f", "etc/user-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 统一错误处理:所有业务错误统一返回 200,由前端根据 code 判断
httpx.SetErrorHandler(func(err error) (int, any) {
return http.StatusOK, biz.Error(biz.NewError(400, err.Error()))
})
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
6.7 JWT 集成
6.7.1 配置文件 etc/user-api.yaml
Go
Name: user-api
Host: 0.0.0.0
Port: 8888
Auth:
JwtSecret: "your-256-bit-secret" # 必须使用足够复杂的密钥
Expire: 3600 # token 有效期(秒)
6.7.2 配置结构体 internal/config/config.go
Go
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
Auth struct {
JwtSecret string
Expire int64
}
}
6.7.3 JWT 生成工具 internal/biz/jwt.go
Go
package biz
import "github.com/golang-jwt/jwt/v4"
func GetJwtToken(secretKey string, iat, seconds int64, payload string) (string, error) {
claims := jwt.MapClaims{
"exp": iat + seconds,
"iat": iat,
"payload": payload,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secretKey))
}
6.7.4 登录逻辑 internal/logic/account/loginlogic.go
Go
func (l *LoginLogic) Login(req *types.LoginReq) (*types.CommonResponse, error) {
// 此处应有真实的用户密码校验(略)
token, err := biz.GetJwtToken(
l.svcCtx.Config.Auth.JwtSecret,
time.Now().Unix(),
l.svcCtx.Config.Auth.Expire,
req.Username, // 将用户标识存入 payload
)
if err != nil {
return biz.Error(biz.ServerError), nil
}
return biz.Success(types.LoginResp{Token: token}), nil
}
6.7.5 受保护路由的 JWT 中间件 internal/handler/routes.go
Go
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/api/address/list",
Handler: address.AddressHandler(serverCtx),
},
},
rest.WithJwt(serverCtx.Config.Auth.JwtSecret), // 启用 JWT 鉴权
)
6.7.6 在业务逻辑中获取用户信息
Go
func (l *AddressLogic) Address() (*types.CommonResponse, error) {
// 从上下文中提取 JWT payload(需在 routes 中配置 rest.WithJwt 后自动注入)
payload, ok := l.ctx.Value("payload").(string)
if !ok {
return biz.Error(biz.UserNotLogin), nil
}
// payload 即登录时存入的用户标识,可据此查询用户详情
// ...
return biz.Success(types.Address{
Id: 1,
Name: "张三",
Phone: "12345678901",
Address: "中国",
}), nil
}
6.8 企业级最佳实践
(1)密钥管理
-
使用足够复杂、长度 ≥ 32 位的密钥,严禁硬编码在代码中。
-
生产环境通过环境变量或配置中心(如 etcd、Nacos)注入。
-
定期轮换密钥,并保证旧 token 在过渡期仍可验证。
(2)过期策略
-
Access Token 有效期一般设为 15 分钟 ~ 2 小时,降低泄露风险。
-
配合
Refresh Token(长期有效)实现无感续期。 -
Refresh Token应存储在服务端(如 Redis),并设置黑名单机制。
(3)安全增强
-
始终通过 HTTPS 传输 token,防止中间人攻击。
-
设置
HttpOnly和Secure的 Cookie 存储 token(若前端支持)。 -
对敏感操作(如修改密码)要求二次认证。
(4)错误处理
-
JWT 解析失败(过期、签名错误)应统一返回 401 状态码,便于前端跳转登录。
-
日志中切勿打印 token 原文,避免泄露。
(5)负载均衡与网关
-
在网关层统一验证 JWT,避免每个微服务重复验证。
-
若使用 go-zero 的
api服务,可直接在routes.go中配置rest.WithJwt。
第七章:go-zero 跨域配置(企业级实践)
7.1 跨域问题根源
浏览器同源策略限制非同源(协议、域名、端口任一不同)的 AJAX 请求。前后端分离项目中,前端(如 React/Vue 应用运行在 http://localhost:5173)与后端 API(如 http://api.example.com)必然跨域,需服务端通过 CORS 响应头允许特定来源访问。
7.2 go-zero 中 CORS 配置方式
在 main.go 中创建服务器时,通过选项配置 CORS:
7.2.1 允许所有来源(仅开发环境)
Go
server := rest.MustNewServer(c.RestConf, rest.WithCors())
7.2.2 允许指定域名(推荐生产环境)
Go
server := rest.MustNewServer(c.RestConf, rest.WithCors("https://app.example.com"))
7.2.3 自定义允许的请求头(如需携带自定义 header)
Go
server := rest.MustNewServer(
c.RestConf,
rest.WithCorsHeaders("Content-Type", "Authorization", "X-Request-ID"),
)
7.2.4 同时指定域名与请求头
Go
server := rest.MustNewServer(
c.RestConf,
rest.WithCors("https://app.example.com"),
rest.WithCorsHeaders("Content-Type", "Authorization"),
)
7.2.5 完全自定义 CORS 策略(适用于复杂需求)
Go
server := rest.MustNewServer(
c.RestConf,
rest.WithCustomCors(func(header http.Header) {
header.Set("Access-Control-Allow-Origin", "https://app.example.com")
header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
header.Set("Access-Control-Allow-Credentials", "true")
}, nil),
)
7.3 企业级 CORS 最佳实践
(1)严格限制来源
-
生产环境绝不允许
*通配符,必须指定具体域名。 -
若前端存在多个域名,可通过配置列表动态生成。
(2)凭证支持
- 如需携带 Cookie 或 Authorization 头,必须设置
Access-Control-Allow-Credentials: true,且此时Origin不能为*。
(3)预检请求优化
-
go-zero 内部已处理
OPTIONS请求,无需额外代码。 -
对于高频接口,可适当延长
Access-Control-Max-Age的缓存时间,减少预检请求。
(4)动态跨域配置
- 可通过读取配置文件(如
cors.AllowOrigins)动态设置,支持不同环境(开发、测试、生产)使用不同规则。
(5)安全头附加
- 可同时设置
X-Content-Type-Options: nosniff、X-Frame-Options: DENY等安全头。
(6)网关层统一处理
- 若使用 Nginx 或 API 网关,建议在网关层统一配置 CORS,避免每个服务重复配置。
7.4 示例:支持多域名与凭证
Go
// 假设从配置文件读取允许的域名列表
allowedOrigins := []string{"https://app1.example.com", "https://app2.example.com"}
originStr := strings.Join(allowedOrigins, " ")
server := rest.MustNewServer(
c.RestConf,
rest.WithCustomCors(func(header http.Header) {
header.Set("Access-Control-Allow-Origin", originStr) // 实际应动态匹配请求的 Origin
header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Max-Age", "86400")
}, nil),
)
第八章:微服务治理深度实践
8.1 服务注册与发现
在微服务架构中,一个应用会被拆分成多个独立部署的服务。它们之间需要相互调用,但服务的 IP 地址和端口可能随时变化。服务注册与发现 就是解决"服务在哪里"的问题。
-
服务注册:服务启动时,将自己的网络地址(IP+端口)上报给一个中心化的注册中心(如 etcd、Consul)。
-
服务发现:调用方从注册中心获取目标服务的地址列表,实现动态路由。
8.1.1 为什么 Web3.0 项目需要它?
假设我们构建了一个链下索引服务,包含 indexer(从链上同步数据)、api(提供查询接口)、alert(价格预警)等多个微服务。它们之间需要相互通信,且可能动态扩缩容。使用服务发现后,api 服务在调用 indexer 的 RPC 接口时,无需硬编码 IP,而是通过注册中心找到可用的节点。
8.1.2 实战:集成 etcd
首先,安装 etcd(或使用 Docker 运行)。然后在 go-zero 的配置文件中指定注册中心地址。
1. 配置 etcd 连接
修改 etc/indexer-api.yaml:
Go
Name: indexer-api
Host: 0.0.0.0
Port: 8888
Etcd:
Hosts:
- 127.0.0.1:2379
Key: indexer.rpc
2. 在 main.go 中启用服务发现
go-zero 的 rest.MustNewServer 会自动读取 Etcd 配置并注册服务。如果我们需要调用下游 RPC 服务,则通过 zrpc.MustNewClient 创建客户端时,也会自动发现服务。
Go
package main
import (
"flag"
"fmt"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
"your-module/internal/config"
"your-module/internal/handler"
"your-module/internal/svc"
)
var configFile = flag.String("f", "etc/indexer-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
解释:
-
c.RestConf中包含了Etcd配置,rest.MustNewServer内部会将该服务注册到 etcd。 -
如果需要调用其他服务,我们可以在
ServiceContext中创建 RPC 客户端,例如:
Go
// internal/svc/servicecontext.go
import "github.com/zeromicro/go-zero/zrpc"
type ServiceContext struct {
Config config.Config
IndexerRpc zrpc.Client
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
IndexerRpc: zrpc.MustNewClient(zrpc.RpcClientConf{
Etcd: c.Etcd,
Key: "indexer.rpc",
}),
}
}
Web3.0 场景 :假设我们有一个 indexer 服务专门负责监听区块链事件并写入数据库,API 服务需要调用它的 RPC 接口获取最新区块高度。通过服务发现,API 服务无需知道 indexer 的具体地址,只要注册中心存在即可。
8.2 限流、熔断与降载
限流:控制单位时间内的请求数量,防止突发流量冲垮系统。
熔断:当下游服务持续出错时,自动切断请求,避免级联故障。
降载:当系统负载过高时,主动拒绝部分请求,保护核心功能。
8.2.1 限流配置
在 .api 文件中,可以通过 @server 的 middleware 或 timeout 来实现简单限流。更精细的限流可使用 go-zero 内置的 limit 包。
示例:基于令牌桶的限流中间件
Go
// internal/middleware/ratelimit.go
package middleware
import (
"net/http"
"github.com/zeromicro/go-zero/core/limit"
"github.com/zeromicro/go-zero/core/stores/redis"
)
func RateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc {
limiter := limit.NewTokenLimiter(100, 10, redis.MustNewRedis(redis.RedisConf{
Host: "127.0.0.1:6379",
Type: "node",
}), "rate-limit")
return func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte("请求过于频繁,请稍后再试"))
return
}
next(w, r)
}
}
然后在 routes.go 中为需要限流的路由组添加该中间件。
Web3.0 场景:NFT 市场的实时行情接口可能会被高频调用,为防止滥用,可以设置每秒 100 次的限流。
8.2.2 熔断与降载
go-zero 的 breaker 包提供了熔断器。通常我们在调用下游服务时使用。
Go
// 调用下游 RPC 时使用熔断
resp, err := l.svcCtx.IndexerRpc.GetLatestBlock(l.ctx, &pb.Empty{})
if err != nil {
// 熔断器会自动统计错误率,当错误率达到阈值时,后续请求直接返回错误,不再实际调用
return nil, err
}
配置熔断参数 :可以在 zrpc.RpcClientConf 中设置 Timeout、KeepAlive 等,熔断器默认启用。
降载 :go-zero 的 load 包可以检测系统负载,当 CPU 使用率超过阈值时,自动拒绝请求。
Go
// 在 main.go 中启用降载
server := rest.MustNewServer(c.RestConf, rest.WithCors())
server.Use(load.SheddingMiddleware(c.LoadShedding))
Web3.0 场景:当链上发生大量交易导致后端数据库压力激增时,可以开启降载,暂时拒绝非核心查询请求,保证写入服务的稳定性。
8.3 分布式链路追踪
微服务调用链可能跨越多个节点,定位问题非常困难。链路追踪 通过为每个请求生成全局唯一的 Trace ID,串联所有日志和调用记录。
8.3.1 集成 Jaeger
go-zero 官方支持 jaeger 追踪。只需在配置文件中开启即可。
Go
# etc/indexer-api.yaml
Telemetry:
Name: indexer-api
Endpoint: http://localhost:14268/api/traces
Sampler: 1.0
Batcher: jaeger
然后,在 main.go 中初始化:
Go
import "github.com/zeromicro/go-zero/core/trace"
func main() {
// ...
trace.StartAgent(c.Telemetry) // 启动链路追踪
defer trace.StopAgent()
// ...
}
Web3.0 场景:当用户发起一笔跨链交易,可能涉及多个服务(用户服务、订单服务、链上监听服务、合约调用服务),通过 Jaeger 可以直观看到整个流程的耗时和错误点。
第九章:配置管理的高级玩法
9.1 多环境配置
企业级项目通常有开发、测试、生产等多个环境,配置各不相同。推荐使用环境变量或配置中心。
1. 使用环境变量覆盖
在 config.go 中,我们可以让 DataSource 从环境变量读取:
Go
type Config struct {
rest.RestConf
DataSource string
}
func (c *Config) Load() error {
c.DataSource = os.Getenv("DB_DSN")
return nil
}
然后启动服务时设置环境变量:
Go
export DB_DSN="postgres://user:pass@host:5432/db"
go run main.go
2. 使用配置文件模板
创建 etc/user-api.dev.yaml 和 etc/user-api.prod.yaml,通过启动参数指定:
Go
go run main.go -f etc/user-api.prod.yaml
9.2 配置中心(Nacos / Apollo)
对于动态配置,可以使用配置中心。go-zero 社区有 nacos 的集成示例。
以 Nacos 为例:
-
安装 Nacos,创建配置
data-id=user-api.yaml,内容为:GoDataSource: "postgres://..." -
在
main.go中从 Nacos 拉取配置:
Go
import "github.com/nacos-group/nacos-sdk-go/v2/clients"
func main() {
// 从 Nacos 获取配置
client, _ := clients.CreateConfigClient(map[string]interface{}{
"serverConfigs": []interface{}{
map[string]interface{}{
"ip": "127.0.0.1",
"port": 8848,
},
},
})
content, _ := client.GetConfig(vo.ConfigParam{
DataId: "user-api.yaml",
Group: "DEFAULT_GROUP",
})
var c config.Config
conf.LoadFromYamlBytes([]byte(content), &c)
// ... 后续使用 c
}
Web3.0 场景:在 NFT 市场运营期间,可能临时调整限流阈值或开启某个特性开关,使用配置中心可以无需重启服务。
第十章:数据访问层高级特性
10.1 事务处理
在 Web3.0 业务中,经常需要保证多个数据库操作的原子性,例如:
-
创建订单时扣减库存、增加用户资产。
-
处理跨链交易时,记录源链交易哈希并更新目标链状态。
使用 go-zero 的 sqlx 事务:
Go
// internal/logic/order/createorderlogic.go
func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderReq) (*types.CommonResponse, error) {
// 开启事务
err := l.svcCtx.FocusModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 1. 扣减库存
// 2. 创建订单记录
// 3. 增加用户积分
// 任意一步失败,自动回滚
return nil
})
if err != nil {
return nil, err
}
return biz.Success("订单创建成功"), nil
}
解释:
-
TransactCtx接收一个闭包,闭包内执行所有数据库操作。 -
传入的
session是事务上下文,所有操作必须使用这个session而不是model的直接连接。 -
如果闭包返回错误,事务自动回滚;返回 nil 则提交。
Web3.0 场景:用户在 DEX 下单,需要同时扣除用户资产、记录订单、更新市场深度。使用事务可以确保三者同时成功或同时失败。
10.2 分库分表与读写分离
当数据量达到千万级别时,单表查询会变慢。我们可以使用分库分表中间件(如 ShardingSphere-JDBC)或 go-zero 的 Database Resolver。
go-zero 的 sqlx 支持读写分离:
Go
// 在配置中定义多个数据库源
type Config struct {
rest.RestConf
MasterDataSource string
SlaveDataSource string
}
// 在 servicecontext.go 中创建两个连接
func NewServiceContext(c config.Config) *ServiceContext {
master := sqlx.NewSqlConn("postgres", c.MasterDataSource)
slave := sqlx.NewSqlConn("postgres", c.SlaveDataSource)
return &ServiceContext{
Config: c,
FocusModel: pgsql.NewFocusModel(master), // 写操作使用 master
FocusModelRO: pgsql.NewFocusModel(slave), // 读操作使用 slave
}
}
然后在逻辑层,查询时使用 FocusModelRO,写入使用 FocusModel。
分表 :需要自己实现路由逻辑,例如根据 user_id 取模决定表名。在 model 层自定义 TableName 方法返回动态表名即可。
10.3 缓存穿透/击穿/雪崩防范
-
缓存穿透:查询不存在的数据,导致每次请求都穿透到数据库。
- 解决方案:布隆过滤器 + 空值缓存。
-
缓存击穿:热点 key 失效,大量请求涌入数据库。
- 解决方案:使用互斥锁或 go-zero 的
singleflight防止并发查询。
- 解决方案:使用互斥锁或 go-zero 的
-
缓存雪崩:大量 key 同时失效。
- 解决方案:设置随机过期时间。
使用 singleflight 防止缓存击穿:
Go
import "golang.org/x/sync/singleflight"
var sf singleflight.Group
func (l *GetFocusLogic) GetFocus(req *types.GetFocusReq) (*types.FocusResp, error) {
// 先从缓存获取
cacheKey := fmt.Sprintf("focus:%d", req.Id)
if val, err := l.svcCtx.Redis.Get(cacheKey); err == nil {
// 命中缓存
var focus types.Focus
json.Unmarshal([]byte(val), &focus)
return &types.FocusResp{Result: focus}, nil
}
// 使用 singleflight 防止击穿
v, err, _ := sf.Do(cacheKey, func() (interface{}, error) {
// 查数据库
focus, err := l.svcCtx.FocusModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, err
}
// 写入缓存
data, _ := json.Marshal(focus)
l.svcCtx.Redis.Setex(cacheKey, string(data), 3600)
return focus, nil
})
if err != nil {
return nil, err
}
return &types.FocusResp{Result: v.(*pgsql.Focus)}, nil
}
Web3.0 场景:链上代币价格查询接口可能会被大量客户端高频调用,使用缓存 + singleflight 可以有效减少对后端数据库的压力。
第十一章:服务部署与运维
11.1 容器化
编写 Dockerfile:
Go
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o indexer-api main.go
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/indexer-api .
COPY etc /app/etc
EXPOSE 8888
CMD ["./indexer-api", "-f", "etc/indexer-api.yaml"]
构建镜像并运行:
Go
docker build -t indexer-api:latest .
docker run -p 8888:8888 indexer-api
11.2 Kubernetes 部署
编写 deployment.yaml:
Go
apiVersion: apps/v1
kind: Deployment
metadata:
name: indexer-api
spec:
replicas: 3
selector:
matchLabels:
app: indexer-api
template:
metadata:
labels:
app: indexer-api
spec:
containers:
- name: indexer-api
image: indexer-api:latest
ports:
- containerPort: 8888
env:
- name: DB_DSN
valueFrom:
secretKeyRef:
name: db-secret
key: dsn
---
apiVersion: v1
kind: Service
metadata:
name: indexer-api-svc
spec:
selector:
app: indexer-api
ports:
- protocol: TCP
port: 8888
targetPort: 8888
健康检查 :在 main.go 中添加 /health 端点:
Go
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
在 K8s 中配置 livenessProbe 和 readinessProbe。
11.3 优雅退出
go-zero 的服务默认支持优雅退出(通过 server.Stop())。我们需要捕获系统信号:
Go
func main() {
// ... 启动服务
go func() {
server.Start()
}()
// 等待信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 优雅关闭
server.Stop()
}
Web3.0 场景:在滚动更新时,优雅退出确保正在处理的请求不会中断,避免用户交易状态丢失。
11.4 日志规范
使用 logx 时,可以配置输出到文件:
Go
# etc/indexer-api.yaml
Log:
Mode: file
Path: logs
Level: info
同时,可以通过 logx.SetWriter 将日志输出到 ELK 等集中式日志系统。
第十二章:测试与质量保障
12.1 单元测试
使用 Go 标准库 testing 和 mockery 生成 mock。
示例 :测试 GetFocusListLogic
Go
// internal/logic/focus/getfocuslistlogic_test.go
package focus
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"your-module/internal/svc"
"your-module/model/pgsql"
)
func TestGetFocusListLogic_GetFocusList(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 创建 mock model
mockModel := pgsql.NewMockFocusModel(ctrl)
mockModel.EXPECT().FindAll(gomock.Any()).Return([]*pgsql.Focus{
{Id: 1, Title: "test"}, {Id: 2, Title: "test2"},
}, nil)
svcCtx := &svc.ServiceContext{FocusModel: mockModel}
l := NewGetFocusListLogic(context.Background(), svcCtx)
resp, err := l.GetFocusList()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(resp.Result) != 2 {
t.Fatalf("expected 2 items, got %d", len(resp.Result))
}
}
12.2 集成测试
可以使用 testcontainers 启动真实的 PostgreSQL 容器进行测试。
Go
func TestIntegration(t *testing.T) {
ctx := context.Background()
postgresContainer, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:13",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{"POSTGRES_PASSWORD": "123456"},
},
Started: true,
})
defer postgresContainer.Terminate(ctx)
// 获取连接信息,初始化数据库,然后执行测试...
}
12.3 压测与性能调优
使用 wrk 或 ab 进行压力测试:
Go
wrk -t12 -c400 -d30s http://localhost:8888/api/focus/list
观察 CPU、内存使用情况,使用 pprof 分析瓶颈:
Go
import _ "net/http/pprof"
// 在 main 中启动 pprof 服务
go func() {
http.ListenAndServe(":6060", nil)
}()
然后访问 http://localhost:6060/debug/pprof/ 查看火焰图。
第十三章:安全纵深
13.1 敏感信息保护
使用 Kubernetes Secret 存储数据库密码、JWT 密钥等。在代码中通过环境变量读取。
13.2 防 SQL 注入
-
始终使用参数化查询,避免拼接 SQL。
-
go-zero 的
sqlx默认使用占位符,生成的原生 SQL 也必须使用?或$1。
13.3 API 防重放攻击
可以结合 Redis 存储请求签名和 timestamp,在一定时间内拒绝相同签名的请求。
13.4 限流高级配置
除了令牌桶,还可以使用漏桶算法,或基于用户 ID 的限流。
Go
func UserRateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-Id")
// 对每个用户单独限流
limiter := limit.NewTokenLimiter(10, 5, redisClient, "user:"+userID)
if !limiter.Allow() {
w.WriteHeader(http.StatusTooManyRequests)
return
}
next(w, r)
}
}
第十四章:消息队列与异步处理
在 Web3.0 中,许多操作是异步的,例如:
-
用户提交交易后,需要等待链上确认,此时可以发送消息到队列,由后端监听链上事件后回调。
-
批量处理 NFT 元数据,避免阻塞主流程。
集成 Kafka :使用 segmentio/kafka-go。
示例:在订单创建后发送消息到 Kafka
Go
func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderReq) (*types.CommonResponse, error) {
// ... 创建订单逻辑
// 发送消息到 Kafka
writer := kafka.NewWriter(kafka.WriterConfig{
Brokers: []string{"localhost:9092"},
Topic: "order-events",
})
msg := map[string]interface{}{
"order_id": orderId,
"user_id": req.UserId,
"action": "created",
}
data, _ := json.Marshal(msg)
err := writer.WriteMessages(l.ctx, kafka.Message{Value: data})
if err != nil {
// 记录错误,但不影响主流程(或使用重试)
}
return biz.Success("订单创建成功"), nil
}
消费端:单独的服务监听 Kafka,处理后续逻辑。
第十五章:第三方服务集成
15.1 集成区块链节点
在 Web3.0 后端中,经常需要调用以太坊节点 RPC。使用 go-ethereum 库。
示例:在 logic 中查询账户余额
Go
import "github.com/ethereum/go-ethereum/ethclient"
func (l *GetBalanceLogic) GetBalance(req *types.GetBalanceReq) (*types.GetBalanceResp, error) {
client, _ := ethclient.Dial(l.svcCtx.Config.EthNodeURL)
balance, _ := client.BalanceAt(l.ctx, common.HexToAddress(req.Address), nil)
return &types.GetBalanceResp{Balance: balance.String()}, nil
}
注意 :应该使用连接池,避免每次请求都建立新连接。可以在 ServiceContext 中创建全局 ethclient.Client。
15.2 集成 IPFS
上传 NFT 元数据到 IPFS:
Go
import ipfs "github.com/ipfs/go-ipfs-api"
func (l *UploadMetadataLogic) UploadMetadata(req *types.UploadReq) (*types.UploadResp, error) {
sh := ipfs.NewShell(l.svcCtx.Config.IPFSNode)
cid, err := sh.Add(strings.NewReader(req.Content))
if err != nil {
return nil, err
}
return &types.UploadResp{Cid: cid}, nil
}
第十六章:代码生成与团队协作
16.1 定制 goctl 模板
goctl 允许自定义代码生成模板,例如修改生成的 model 文件,统一添加 soft delete 字段。
在项目根目录下创建 .goctl 文件夹,放入自定义模板,然后生成时指定模板路径:
Go
goctl model pg datasource --url "..." --table focus --dir ./model/pgsql --template .goctl
16.2 API 文档自动化
使用 goctl api doc 生成 Markdown,再通过工具转换为 HTML。也可以直接生成 OpenAPI 3.0 格式,集成 Swagger UI。
Go
goctl api doc --api shop.api --dir ./doc
然后使用 swagger-ui 或 redoc 展示生成的文档。
第十七章:总结与展望
通过本讲义,你已经系统学习了 go-zero 在企业级 Web3.0 开发中的高级应用,包括:
-
微服务治理(注册发现、限流熔断、链路追踪)
-
配置管理(多环境、配置中心、热更新)
-
数据访问层(事务、分库分表、缓存策略)
-
部署运维(容器化、K8s、健康检查、优雅退出)
-
测试与质量保障
-
安全纵深
-
异步处理与第三方服务集成
-
团队协作与代码生成
这些能力将帮助你构建一个稳定、可扩展、易维护的 Web3.0 后端系统。接下来,你可以结合具体的业务需求(如 DEX 撮合引擎、NFT 市场、链上数据索引)进行实战,将所学知识应用到真实场景中。
