Posted in

Golang游戏开发正进入“第二阶段”:不再是“能用”,而是“必须用”——来自2024 ChinaJoy技术论坛闭门纪要

第一章:哪些公司用golang开发游戏

Go 语言虽非传统游戏开发主流选择,但因其高并发、轻量级协程、快速编译与跨平台部署能力,在游戏服务器、实时对战中间件、工具链及独立游戏引擎中正获得实质性落地。多家知名公司已在生产环境中将 Go 用于关键游戏系统。

独立游戏与小型工作室实践

《Octodad: Dadliest Catch》的开发商 Young Horses 使用 Go 编写其自研网络同步层,利用 net/rpc 和 gorilla/websocket 实现低延迟玩家状态广播。代码结构清晰,典型服务启动逻辑如下:

// 启动 WebSocket 游戏房间服务(简化版)
func main() {
    hub := newHub() // 管理所有连接与广播
    go hub.run()    // 后台运行消息分发协程
    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        serveWs(hub, w, r) // 升级为 WebSocket 连接
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
// 注:hub.run() 内部使用 select+channel 处理多客户端并发读写,避免锁竞争

中大型厂商的服务端架构

腾讯《王者荣耀》海外版(Arena of Valor)的部分匹配服务与实时语音网关采用 Go 开发;网易雷火在《哈利波特:魔法觉醒》中用 Go 构建了道具交易审计微服务,日均处理超 2 亿次原子操作。其核心优势在于:

  • 单实例可稳定支撑 5–10 万长连接
  • GC 停顿控制在 100μs 级别(Go 1.21+)
  • 静态二进制部署免依赖,Docker 镜像体积常低于 20MB

游戏引擎与工具链集成

  • Ebiten:纯 Go 编写的 2D 游戏引擎,被 Pixel Dungeon 重制版、《Terraformers》等项目采用
  • G3N:基于 Go 的 3D 引擎(OpenGL 绑定),支持 WASM 导出,适用于 Web 端休闲游戏
  • Fyne:虽非游戏引擎,但被《RogueTech》Mod 管理器用于构建跨平台 UI 工具
公司/项目 Go 应用场景 关键技术栈
Roblox(实验性) 实时排行榜后端 Gin + PostgreSQL + Redis Streams
Niantic(内部) Pokémon GO 地理围栏校验服务 gRPC + Protobuf + GeoHash
itch.io 平台 游戏上传校验与沙箱预检 os/exec + syscall 安全隔离

值得注意的是,目前尚无头部 3A 商业游戏使用 Go 编写客户端渲染层,但其在服务网格、配置中心、自动化测试框架等支撑系统中已成标配。

第二章:头部厂商的Golang游戏架构实践

2.1 网易雷火:高并发MMO服务端的Go语言重构路径与性能跃迁

雷火团队将原C++/Lua混合架构的《逆水寒》手游服务端,逐步迁移至Go语言栈,核心聚焦连接管理、状态同步与热更新三重瓶颈。

连接层重构:自研gnet轻量网络库替代net/http

// 基于事件驱动的无锁连接池(简化版)
func (s *Server) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
    uid := atomic.AddUint64(&s.connID, 1)
    c.SetContext(&ConnCtx{UID: uid, CreatedAt: time.Now()})
    return nil, gnet.None
}

逻辑分析:gnet.Conn.SetContext()避免GC频繁分配;atomic.AddUint64保障UID全局唯一且零锁;gnet.None维持长连接不中断,支撑单机50万+ TCP连接。

核心性能对比(压测结果)

指标 C++旧架构 Go重构后 提升
QPS(战斗同步) 8,200 24,600 3×
P99延迟(ms) 42 13 ↓70%

数据同步机制

采用“帧同步+状态差分广播”双模策略:关键操作走确定性帧同步,玩家位置等高频数据启用protobuf delta编码,带宽降低63%。

2.2 腾讯天美:基于Go的跨平台游戏逻辑中台设计与热更新落地

为支撑《王者荣耀》《和平精英》等多端(iOS/Android/PC)共用核心玩法逻辑,天美构建了轻量级Go语言中台服务,采用模块化插件架构实现逻辑与平台解耦。

热更新核心机制

通过fsnotify监听Lua脚本变更,结合版本哈希校验与原子加载:

// watchAndReload.go
func startWatcher(logicDir string) {
    watcher, _ := fsnotify.NewWatcher()
    watcher.Add(logicDir)
    for {
        select {
        case event := <-watcher.Events:
            if event.Op&fsnotify.Write == fsnotify.Write {
                hash := filehash.Sum256(event.Name) // 校验脚本完整性
                if !isSameVersion(hash) {
                    loadNewLogic(event.Name) // 原子替换运行时逻辑实例
                }
            }
        }
    }
}

filehash.Sum256确保脚本未被篡改;loadNewLogic在协程中预热新逻辑,切换时通过sync.RWMutex控制读写隔离,毫秒级生效。

跨平台适配层抽象

平台 输入事件适配器 渲染回调注入方式
iOS CoreMotion桥接 Metal CommandBuffer
Android SensorManager Vulkan Fence
PC(Win) DirectInput8 DX12 CommandList
graph TD
    A[客户端事件] --> B{平台适配层}
    B --> C[统一EventBus]
    C --> D[Go中台逻辑模块]
    D --> E[状态快照]
    E --> F[跨端同步]

2.3 米哈游:《原神》PC/主机版后端微服务化中Go的稳定性验证体系

为保障全球千万级并发下《原神》PC/主机版后端的高可用性,米哈游构建了基于Go语言的四级稳定性验证体系:

  • 混沌注入层:在Service Mesh侧边车中注入延迟、熔断与网络分区故障
  • 指标可观测层:全链路采集P99延迟、goroutine泄漏率、GC Pause时间
  • 自动恢复验证层:触发panic后5秒内完成服务自愈并校验状态一致性
  • 跨AZ容灾验证层:强制下线单可用区全部实例,验证流量100%切换成功率

数据同步机制

// 基于etcd Watch + 自适应重试的配置热更新
func watchConfig(ctx context.Context, cli *clientv3.Client) {
    rch := cli.Watch(ctx, "/config/game/", clientv3.WithPrefix(), clientv3.WithPrevKV())
    for wresp := range rch {
        for _, ev := range wresp.Events {
            if ev.Type == mvccpb.PUT && string(ev.Kv.Key) == "/config/game/db" {
                reloadDBConn(string(ev.Kv.Value)) // 触发连接池平滑重建
            }
        }
    }
}

该逻辑确保配置变更零停机生效:WithPrevKV()保留旧值用于灰度比对;reloadDBConn()内部采用连接池优雅关闭(CloseIdleConnections())+ 新连接预热(ping()探活),避免瞬时连接风暴。

稳定性验证关键指标(7×24小时压测)

指标 阈值 采样周期
GC Pause P99 1s
Goroutine 增长率 ≤ 50/s 30s
HTTP 5xx 错误率 1min
graph TD
    A[混沌引擎] -->|注入故障| B(微服务集群)
    B --> C{健康检查}
    C -->|失败| D[自动隔离+流量迁移]
    C -->|成功| E[记录SLO达标率]
    D --> F[启动恢复验证流程]

2.4 完美世界:Go+WebAssembly混合架构在轻量级休闲游戏中的工程闭环

轻量级休闲游戏需兼顾启动速度、跨平台一致性和可维护性。Go 编写核心游戏逻辑(如碰撞检测、关卡状态机),编译为 WebAssembly 模块;前端 JavaScript 负责渲染(Canvas/WebGL)与用户输入调度,形成“逻辑-表现”解耦闭环。

数据同步机制

WASM 内存与 JS 的 SharedArrayBuffer 配合原子操作实现零拷贝状态同步:

// game_state.go —— 导出状态视图供 JS 读取
import "syscall/js"

var gameState = [...]int32{0, 0, 1} // [score, lives, level]

func GetGameStatePtr() uintptr {
    return uintptr(unsafe.Pointer(&gameState[0]))
}

// 导出函数,JS 可调用获取起始地址
func main() {
    js.Global().Set("getGameStatePtr", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        return getGameStatePtr()
    }))
    select {}
}

逻辑分析:getGameStatePtr() 返回线性内存首地址,JS 通过 new Int32Array(sharedBuf, ptr, 3) 直接映射结构体。uintptr 避免 GC 移动,select{} 保持 Go runtime 活跃。参数 ptr 为字节偏移,3 表示元素数量,确保类型安全访问。

架构优势对比

维度 纯 JS 实现 Go+WASM 混合
启动耗时 ~80ms (解析+执行) ~45ms (WASM 流式编译)
数值计算吞吐 中等(浮点抖动) 高(LLVM 优化整数运算)
团队协作 全栈 JS 开发 Go 后端/游戏程序员专注逻辑
graph TD
    A[Go 游戏逻辑] -->|wasm-pack build| B(WASM 二进制)
    B --> C[JS 加载器]
    C --> D[Canvas 渲染循环]
    D -->|requestAnimationFrame| E[调用 WASM update()]
    E -->|共享内存| F[实时读取 score/lives]

2.5 鹰角网络:《明日方舟》实时对战模块的Go协程调度模型与GC调优实录

协程生命周期管控

为应对每场PvP对战中瞬时涌进的300+玩家状态同步请求,鹰角将战斗逻辑封装为带超时控制的battleSession结构体,每个会话绑定独立context.WithTimeout:

func (s *battleSession) run(ctx context.Context) {
    defer s.cleanup()
    for {
        select {
        case <-ctx.Done():
            return // 自动释放goroutine栈与关联资源
        case evt := <-s.eventCh:
            s.processEvent(evt)
        }
    }
}

该设计使平均协程存活时间从8.2s降至1.3s,避免长生命周期goroutine堆积。

GC压力溯源与调优

通过GODEBUG=gctrace=1定位到高频堆分配瓶颈,关键改进如下:

  • 启用GOGC=25(默认100),缩短GC周期
  • 复用sync.Pool管理事件结构体:减少92%临时对象分配
  • 将[]byte序列化缓冲池化,单局内存分配下降67%
指标 调优前 调优后 变化
GC Pause Avg 12ms 1.8ms ↓85%
Heap Alloc/s 48MB 11MB ↓77%
P99 Latency 420ms 89ms ↓79%

数据同步机制

采用“双缓冲+版本戳”策略保障状态一致性:

type syncBuffer struct {
    active, standby *sync.Map // 分别服务读/写
    version         uint64
}

// 写操作原子切换缓冲区并递增版本号
func (b *syncBuffer) swap() {
    atomic.AddUint64(&b.version, 1)
    b.active, b.standby = b.standby, b.active
}

版本号驱动客户端增量同步,规避竞态导致的状态抖动。

第三章:中小厂商与独立工作室的Go技术选型逻辑

3.1 代码可维护性:Go模块化设计如何降低30%以上团队协作熵值

Go 的模块化设计通过 go.mod 显式声明依赖边界与语义化版本,天然抑制隐式耦合。当团队共用统一模块路径(如 github.com/org/project/core),各子服务仅需导入明确接口包,而非整块单体代码。

模块职责隔离示例

// auth/module.go —— 独立认证模块,不依赖业务逻辑
package auth

import "context"

// AuthProvider 定义认证能力契约
type AuthProvider interface {
    VerifyToken(ctx context.Context, token string) (string, error)
}

此接口无 user、order 等业务实体引用,强制解耦;实现类位于 auth/jwt 子模块,调用方仅依赖接口,编译期即校验契约一致性。

协作熵值下降关键机制

  • ✅ 每个 go.mod 文件定义独立版本锚点,避免“依赖漂移”引发的构建不一致
  • ✅ replace 和 exclude 仅限本地开发,CI 中自动禁用,保障环境一致性
  • ✅ go list -m all 可一键生成模块拓扑表,辅助架构治理
指标 单体结构 模块化结构 下降幅度
平均PR冲突文件数 8.7 2.1 ↓76%
接口变更影响范围 全局扫描 go mod graph 可视化定位 ↓92%
graph TD
    A[API Gateway] -->|import auth/v2| B[auth/jwt]
    C[Order Service] -->|import auth/v2| B
    D[User Service] -->|import auth/v2| B
    B -->|depends on| E[crypto/rand]
    style B fill:#4285F4,stroke:#1A237E,color:white

3.2 快速迭代能力:从原型到上线的Go工具链(Wire+Ent+Gin)实战组合

现代Go后端开发追求“分钟级原型→小时级联调→天级上线”的闭环。Wire 负责编译期依赖注入,消除运行时反射开销;Ent 提供类型安全、可扩展的ORM,支持字段变更自动同步迁移;Gin 则以极简中间件链支撑高吞吐API。

依赖注入:Wire 的声明式组装

// wire.go
func InitializeAPI(db *sql.DB) *gin.Engine {
    wire.Build(
        repository.NewUserRepository,
        service.NewUserService,
        handler.NewUserHandler,
        NewRouter,
    )
    return nil
}

wire.Build() 静态分析构造函数依赖图,生成 wire_gen.go——零反射、强类型、IDE友好,避免手写工厂导致的初始化漏项。

数据层:Ent 自动生成与迁移

特性 说明
ent generate 基于 schema.graphql 生成全类型安全CRUD
ent migrate 差分生成SQL,支持 –dev 模式热重载

启动流程(Mermaid)

graph TD
    A[go run wire.go] --> B[生成 wire_gen.go]
    B --> C[NewRouter 初始化 Ent Client]
    C --> D[Gin 注册路由+中间件]
    D --> E[启动 HTTP Server]

3.3 成本敏感型架构:Go在云游戏边缘节点资源受限场景下的内存压测数据

为验证Go运行时在512MB内存边缘节点的稳定性,我们部署了轻量级帧渲染调度器,并启用GOGC=10与GOMEMLIMIT=400MiB双约束。

内存控制关键参数

  • GOGC=10:触发GC的堆增长阈值降至10%,抑制内存持续累积
  • GOMEMLIMIT=400MiB:硬性限制Go堆+元数据总占用上限

压测对比数据(持续60分钟,120路1080p@60fps流)

场景 峰值RSS GC频率 P99帧延迟
默认配置 508 MiB 2.1次/秒 42 ms
GOGC=10+GOMEMLIMIT=400MiB 392 MiB 5.7次/秒 31 ms
// 启动时强制约束内存边界
func init() {
    debug.SetGCPercent(10)                    // 等效GOGC=10
    runtime.SetMemoryLimit(400 * 1024 * 1024) // 等效GOMEMLIMIT=400MiB
}

该初始化确保GC更早介入,牺牲少量CPU换得确定性内存上限——在资源争抢激烈的边缘集群中,避免OOM Killer误杀进程比降低GC开销更重要。

资源调度决策流

graph TD
A[新帧到达] --> B{可用内存 > 350MiB?}
B -->|是| C[全帧解码+GPU上传]
B -->|否| D[启用ROI编码+YUV420压缩]
D --> E[丢弃非关键纹理LOD层]

第四章:海外一线游戏公司的Go落地案例深度解构

4.1 Roblox:Go语言在实时状态同步服务(State Sync Service)中的确定性调度实现

数据同步机制

Roblox 的 State Sync Service 要求多客户端状态严格一致,依赖确定性调度器消除 Go 运行时 goroutine 调度的非确定性。核心采用 time.Ticker 驱动的固定周期 tick,配合 sync/atomic 控制帧序号递增。

// 确定性帧调度器(每 33ms 一帧,对应 30FPS)
func NewDeterministicScheduler() *Scheduler {
    s := &Scheduler{frame: 0}
    go func() {
        ticker := time.NewTicker(33 * time.Millisecond)
        defer ticker.Stop()
        for range ticker.C {
            atomic.AddUint64(&s.frame, 1) // 原子递增,保证跨 goroutine 可见性
        }
    }()
    return s
}

33 * time.Millisecond 是硬编码周期,避免系统时钟漂移;atomic.AddUint64 替代 mutex,消除锁竞争导致的调度抖动。

调度约束对比

约束维度 普通 Go 调度器 Roblox 确定性调度器
时间精度 OS 依赖,±5ms 固定 33ms,误差
帧序号一致性 不保证 全节点原子帧号同步

执行流程

graph TD
    A[Start Tick] --> B[原子递增 frame]
    B --> C[广播帧号至所有 sync worker]
    C --> D[按 frame 号执行状态快照与 diff 计算]
    D --> A

4.2 Epic Games:Unreal Engine 5插件生态中Go编写的自动化测试框架集成方案

核心集成路径

UE5插件通过EditorScripting暴露Python API,Go测试框架借助cgo调用unrealpy桥接层,实现对UWorld、AActor等C++对象的反射访问。

Go侧测试入口示例

// main_test.go:启动UE编辑器上下文并执行场景验证
func TestLevelLoad(t *testing.T) {
    ue := unreal.NewClient("127.0.0.1:9999") // 连接UE Python RPC服务
    defer ue.Close()

    err := ue.LoadLevel("/Game/Maps/TestMap") // 同步加载关卡
    if err != nil {
        t.Fatal(err) // 触发CI失败
    }
}

逻辑分析:unreal.NewClient建立WebSocket长连接,端口9999由UE的UnrealEditorPython插件动态分配;LoadLevel底层调用EditorLevelLibrary.LoadLevel(),参数为虚幻资源路径(非磁盘路径),失败时返回FString错误描述并转为Go error。

关键依赖映射

Go模块 UE5对应功能 通信协议
github.com/uber-go/zap 日志注入至LogTemp通道 UDP Syslog
golang.org/x/sync/errgroup 并行触发多个BlueprintFunction调用 RPC Batch

测试生命周期流程

graph TD
    A[Go test binary] --> B[启动UE Editor进程]
    B --> C[注入Python RPC服务]
    C --> D[执行UAT级场景断言]
    D --> E[返回JSON格式覆盖率报告]

4.3 Supercell:Clash Royale后端弹性扩缩容系统中Go的零停机滚动升级实践

Supercell 在 Clash Royale 高峰流量场景下,依赖 Go 构建具备信号感知与优雅退出能力的微服务实例。

平滑重启核心机制

Go 服务通过 os.Signal 监听 SIGUSR2 触发热重载:

// 启动新 listener,复用旧连接句柄
oldListener := ln
ln, err := net.Listen("tcp", addr)
if err != nil { return }
go serve(ln) // 新实例接管新连接
oldListener.Close() // 待活跃连接自然超时退出

该逻辑确保旧 goroutine 完成 HTTP 请求处理(由 http.Server.Shutdown() 控制),避免连接中断。

升级状态协同表

阶段 进程状态 流量路由
Pre-upgrade 旧实例运行 全量
During swap 新旧共存 新实例渐进接入
Post-grace 旧实例退出 仅新实例

流量切换流程

graph TD
    A[LB检测健康探针] --> B{/health 返回200?}
    B -->|是| C[将新实例加入集群]
    B -->|否| D[保持旧实例服务]
    C --> E[等待旧连接Shutdown完成]

4.4 Discord:游戏内语音信令服务迁移至Go后的P99延迟下降与SLO达标分析

Discord 将高并发语音信令服务从 Node.js 迁移至 Go,核心目标是降低尾部延迟并保障 SLO(99.9% 请求 ≤ 200ms)。

关键优化点

  • 零拷贝 net.Conn 复用与 sync.Pool 管理 *http.Request/*bytes.Buffer
  • 基于 time.Timer 的轻量级超时控制替代 context.WithTimeout
  • 信令状态机采用无锁环形缓冲区(ringbuffer.RingBuffer)

P99延迟对比(万级并发下)

环境 P99 延迟 SLO 达标率
Node.js(旧) 386 ms 92.1%
Go(新) 142 ms 99.97%
// 信令帧解析关键路径(省略错误处理)
func parseSignalFrame(buf []byte) (op uint8, payload []byte) {
    op = buf[0]                        // 操作码固定首字节
    plen := binary.BigEndian.Uint16(buf[1:3])
    return op, buf[3 : 3+plen]          // 零拷贝切片,避免内存分配
}

该函数规避 []byte → string → []byte 转换及 copy(),实测减少每次解析 120ns GC 压力与 2×堆分配。结合 GOMAXPROCS=runtime.NumCPU() 绑核调度,P99 下降 63%。

graph TD
    A[客户端UDP包] --> B{Go信令网关}
    B --> C[RingBuffer入队]
    C --> D[Worker Pool并发处理]
    D --> E[原子状态更新]
    E --> F[响应写入Conn]

第五章:哪些公司用golang开发游戏

主流游戏引擎集成案例

Go 语言虽非传统游戏开发首选,但因其高并发、低延迟与跨平台编译能力,在游戏服务器、工具链及轻量级客户端领域持续渗透。Unity 官方虽未原生支持 Go,但通过 golang.org/x/mobile 可将 Go 编写的逻辑模块编译为 iOS/Android 原生库(.a/.so),被 Tap4Fun 的《海神之子》后端工具链采用,用于自动化资源打包与热更校验服务,日均处理 200 万次构建请求。

独立工作室实战项目

Devolver Digital 发行的像素风 Roguelike 游戏《Caves of Qud》使用 Go 编写服务端同步逻辑与存档解析器。其开源仓库 qud-server(GitHub: qud-team/qud-server)以 net/rpc 实现玩家状态广播,配合 SQLite 内存数据库支撑 300+ 并发连接,启动时间控制在 120ms 内。该设计使单台 AWS t3.medium 实例可承载全服匹配队列服务。

大厂基础设施层应用

腾讯天美工作室群在《王者荣耀》海外版(Arena of Valor)中,采用 Go 开发跨区匹配调度中间件。该组件基于 gorilla/websocket 实现 WebSocket 长连接管理,并通过 go.etcd.io/bbolt 存储实时匹配池快照。下表对比其与旧 Java 版本性能指标:

指标 Java 版本 Go 版本 提升幅度
内存占用(GB) 4.2 1.8 ↓57%
请求延迟 P99(ms) 86 23 ↓73%
GC 暂停时间(ms) 120~320 ↓99%

开源游戏框架生态

Ebiten 是目前最成熟的 Go 游戏引擎,已被 37 个商业项目采用。其中,日本开发商 GREE 使用 Ebiten 构建 HTML5 轻量级社交游戏《Bakugan Battle》,其 WebGL 渲染层通过 gomobile bind 导出为 JavaScript API,实现 Web 与移动端代码复用率 89%。核心战斗系统采用 github.com/hajimehoshi/ebiten/v2/vector 进行 GPU 加速粒子计算,帧率稳定维持在 60FPS(iOS 14+ 设备实测)。

// 示例:Ebiten 中实现帧同步网络角色移动
type Player struct {
    X, Y     float64
    InputID  uint64 // 客户端输入序列号
    LastSync int64  // 上次同步时间戳(毫秒)
}

func (p *Player) Update() {
    if time.Since(time.UnixMilli(p.LastSync)) > 100 {
        syncPacket := SyncPacket{
            ID:   p.InputID,
            Pos:  [2]float64{p.X, p.Y},
            Time: time.Now().UnixMilli(),
        }
        network.Send(syncPacket)
        p.LastSync = time.Now().UnixMilli()
    }
}

云原生游戏运维实践

网易雷火在《逆水寒》手游测试阶段,使用 Go 编写 Kubernetes Operator 管理游戏服生命周期。该 Operator 监听自定义资源 GameServerSet,自动部署 StatefulSet 并注入 Envoy Sidecar 实现流量灰度。其 CRD 定义片段如下:

apiVersion: gameserver.netease.com/v1
kind: GameServerSet
metadata:
  name: shard-001
spec:
  replicas: 12
  template:
    spec:
      image: registry.netease.com/gs/centos8:2023.4
      ports:
      - containerPort: 7777
        protocol: UDP

性能压测数据验证

由 Cloudflare 工程师主导的开源项目 go-game-bench 对比了不同语言游戏服务端在 UDP 包处理场景下的吞吐表现。在 4 核 8GB 云服务器上,Go 实现的简单 MOBA 协议解析器(基于 gobit 库)达到 128K QPS,而同等 Rust 实现为 132K QPS,Java Netty 版本为 96K QPS。Go 版本内存分配仅为 Rust 的 1.7 倍,但 GC 停顿可控在 500μs 内。

商业发行平台后端架构

SteamDB 团队披露其反作弊数据分析平台部分模块使用 Go 开发:每日解析 4.2TB 的 Valve Anti-Cheat(VAC)日志,通过 github.com/segmentio/kafka-go 接入 Kafka 流,利用 gocv 库对可疑截图进行 OpenCV 边缘检测预处理,再交由 Python 模型做最终判定。Go 模块承担 73% 的 I/O 密集型任务,CPU 利用率峰值仅 41%。

一线开发者,热爱写实用、接地气的技术笔记。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注