Posted in

【Golang中国创始人韩茹独家访谈】:首次公开Golang中文社区从0到100万开发者的3大生死决策

第一章:Golang中国创始初心与社区使命

Go语言自2009年开源以来,其简洁、高效、并发友好的设计哲学迅速在全球开发者中引发共鸣。在中国,Go的早期传播并非始于商业驱动,而是由一批一线工程师自发组织的技术布道者所推动——他们目睹了传统企业级开发中C++的复杂性、Java的臃肿以及Python在高并发场景下的性能瓶颈,迫切需要一种能兼顾开发效率与系统能力的语言。2013年前后,北京、上海、深圳等地陆续出现线下Go技术沙龙,GitHub上首个中文Go教程仓库(如go-zh)被持续共建,标志着本土化实践的萌芽。

社区自治的底层逻辑

中国Go社区从诞生起即践行“代码即文档、贡献即资格”的开源信条。核心维护者不依赖公司背书,而是通过可验证的PR质量、RFC提案参与度及中文文档翻译完整性获得社区信任。例如,golang.org/x/子模块的中文镜像站(如goproxy.cn)由志愿者独立运营,其服务稳定性常年维持99.99%,背后是自动化健康检查脚本与多节点灾备机制:

# goproxy.cn 自动巡检示例(每日执行)
curl -s -o /dev/null -w "%{http_code}" https://goproxy.cn/healthz
# 返回200表示服务正常;非200则触发企业微信告警

中文生态建设的关键行动

  • 文档本地化:完成go.dev全量中文翻译,并保持与英文版同步更新(平均延迟
  • 教育普惠:发起“Go青年计划”,向高校提供免费实验平台与认证考试券,已覆盖127所院校
  • 工程落地支持:建立《Go中国生产环境最佳实践》白皮书,涵盖内存泄漏排查、pprof深度调优等实战指南

使命的延续性表达

社区拒绝将Go工具链“黑盒化”。所有教学材料均要求附带可运行的最小复现案例,例如HTTP服务压测对比实验:

场景 Go(net/http) Node.js(Express) QPS(16核/32GB)
纯文本响应 82,400 36,100 +128%优势
JSON序列化 51,900 28,300 +83%优势

这种以实证为根基的传播方式,使Go在中国既成为云原生基础设施的默认语言,也成长为新一代开发者理解系统本质的启蒙载体。

第二章:从0到10万:冷启动期的五大关键决策

2.1 开源治理模型选择:CNCF标准 vs 本土化自治实践

CNCF 治理强调分层决策与中立基金会托管,而国内头部开源项目常采用“双轨制”:核心委员会+领域SIG自治。

治理结构对比

维度 CNCF 标准 本土化自治实践
决策主体 TOC + 技术监督委员会 项目管理委员会(PMC)+ 社区理事会
代码准入 严格CLA + DCO + 多级PR评审 DCO + 双签机制(企业+个人Maintainer)

典型准入流程(mermaid)

graph TD
    A[PR提交] --> B{CLA/DCO校验}
    B -->|通过| C[自动CI测试]
    C --> D[领域Maintainer初审]
    D --> E[PMC终审合入]
    B -->|失败| F[阻断并提示签署]

自动化合规检查脚本示例

# .github/workflows/compliance.yml
- name: Verify DCO
  uses: endocode/dco-action@v0.5.0
  with:
    # enforce: true  # 强制拦截未签名PR
    skip_labels: "no-dco-required"  # 支持白名单绕过

该脚本调用 dco-action 验证 Git 提交签名,skip_labels 参数支持灰度场景;若启用 enforce,则未签署 DCO 的 PR 将被 CI 直接拒绝。

2.2 中文文档翻译策略:语义对齐优先还是开发者体验优先

在开源项目本地化实践中,两类策略常产生张力:

  • 语义对齐优先:严格忠实原文术语、句式与技术指涉(如将 “callback” 直译为“回调函数”,保留 Promise.then() 的链式结构)
  • 开发者体验优先:重构表达以适配中文技术语境(如用“链式调用”替代直译的“then 方法调用”,将 “stateless component” 译为“无状态组件”而非“无状态的组件”)

术语一致性保障机制

# i18n/zh-CN/glossary.yaml
- source: "hook"
  target: "钩子函数"  # 非字面译“挂钩”,兼顾语义准确与社区通用性
  context: "React 16.8+ 特性,用于在函数组件中使用状态等 React 特性"

该配置驱动自动化校验工具扫描全部 .md 文件,确保 hook 在全文档中统一译为“钩子函数”,避免混用“钩子”“Hook”“反应钩子”等歧义表述。

策略权衡决策表

维度 语义对齐优先 开发者体验优先
术语准确性 ⭐⭐⭐⭐⭐(强保真) ⭐⭐⭐(需人工校准)
学习迁移成本 高(需对照英文概念) 低(符合中文技术思维惯性)
维护复杂度 低(机器辅助+术语库) 高(依赖资深中文技术写作者)
graph TD
  A[原始英文文档] --> B{翻译策略选择}
  B -->|语义对齐| C[术语库强制映射 + 句法保留]
  B -->|开发者体验| D[上下文感知重写 + 案例本土化]
  C --> E[高一致性/低可读性风险]
  D --> F[高可读性/术语漂移风险]

2.3 社区基础设施选型:自研论坛系统 vs GitHub Discussions深度集成

在轻量级开源项目中,社区互动基础设施的选型直接影响维护成本与用户参与度。GitHub Discussions 提供开箱即用的身份统一、Issue 关联、搜索与通知能力;而自研论坛虽可定制 UI 与数据主权,却需承担鉴权、反垃圾、全文检索等全栈复杂性。

核心权衡维度

维度 GitHub Discussions 自研论坛系统
部署成本 零配置,自动启用 ≥3人月(含 CI/CD 集成)
用户登录体验 复用 GitHub OAuth 需独立实现 SSO 或密码体系
数据可移植性 API 可导出 JSON(限 10k 条) 全量数据库直读

同步机制示例(双向评论桥接)

// sync-discussions-to-forum.ts
const syncComment = async (issueId: string, body: string) => {
  const forumPost = await createForumPost({ 
    title: `Re: Issue #${issueId}`, 
    content: body,
    tags: ["github-import"] 
  });
  // ⚠️ 注意:需幂等处理 webhook 重复触发
  await upsertSyncRecord({ issueId, forumId: forumPost.id, timestamp: Date.now() });
};

该函数通过 upsertSyncRecord 确保同一 GitHub 评论仅同步一次,tags 字段便于后续审计与过滤。

架构决策流

graph TD
  A[新项目启动] --> B{社区规模预估}
  B -->|<50 活跃贡献者| C[直接启用 Discussions]
  B -->|>200+ 且需私有化| D[评估自研或 Discourse]
  C --> E[通过 Webhook + Actions 实现轻量扩展]

2.4 首批KOL共建机制:技术影响力评估与非货币激励设计

技术影响力量化模型

采用加权多维指标融合:GitHub Star增速、PR采纳率、技术博客引用数、社区问答响应质量(人工标注+LLM评分)。

非货币激励分层设计

  • ✅ 认证权益:专属徽章、文档联合署名权、闭门技术圆桌席位
  • ✅ 成长通道:直通核心贡献者评审、模块Owner提名资格
  • ❌ 排除积分兑换、现金补贴等货币化路径

核心评估代码逻辑

def calculate_influence_score(repo_stats, blog_metrics, community_engagement):
    # repo_stats: {'star_growth_30d': 127, 'merged_prs': 8}
    # blog_metrics: {'ref_count': 42, 'avg_read_time_sec': 318}
    # community_engagement: {'helpful_answers': 29, 'LLM_quality_score': 0.87}
    return (
        repo_stats['star_growth_30d'] * 0.3 +
        repo_stats['merged_prs'] * 1.5 +
        blog_metrics['ref_count'] * 0.8 +
        blog_metrics['avg_read_time_sec'] / 60 * 0.2 +
        community_engagement['helpful_answers'] * 1.2 +
        community_engagement['LLM_quality_score'] * 50
    )  # 权重经A/B测试校准,确保高质内容权重高于数量指标

激励触发流程

graph TD
    A[月度数据采集] --> B[影响力得分计算]
    B --> C{≥85分?}
    C -->|是| D[授予“架构布道师”徽章 + 圆桌邀请]
    C -->|否| E[推送个性化成长建议]

2.5 早期合规边界设定:开源协议兼容性审查与内容安全风控体系

在项目启动初期,需同步建立双轨风控机制:协议合规性前置扫描与内容安全策略注入。

开源协议兼容性自动化校验

使用 license-checker 结合自定义规则集识别冲突组合:

# 扫描依赖树并过滤GPL/LGPL等传染性协议
npx license-checker \
  --only-allow "MIT,Apache-2.0,BSD-3-Clause" \
  --exclude "devDependencies" \
  --json > licenses.json

逻辑分析:--only-allow 定义白名单协议集合,拒绝 GPL-3.0 等强约束协议;--exclude 避免开发期工具链误报;输出 JSON 便于后续 CI 流水线解析与阻断。

内容安全风控矩阵

风控层级 检查项 响应动作
代码层 敏感词硬编码 构建失败
依赖层 未签名二进制包 自动拦截
文档层 外部链接未备案 PR 标记告警

合规决策流程

graph TD
  A[扫描 package-lock.json] --> B{协议是否在白名单?}
  B -->|是| C[进入内容安全扫描]
  B -->|否| D[终止构建并推送告警]
  C --> E{含敏感模式?}
  E -->|是| D
  E -->|否| F[允许合并]

第三章:从10万到50万:规模化跃迁的三大临界点突破

3.1 Go Module生态适配:中文依赖镜像站架构演进与缓存一致性实践

早期镜像站采用被动拉取+本地文件系统缓存,面临模块版本覆盖冲突与校验失败频发问题。演进至二级缓存架构:CDN边缘节点缓存热模块(TTL 1h),中心集群使用 RocksDB 存储模块元数据与 go.mod 校验和。

数据同步机制

中心集群通过 goproxy 的 ProxyMode 接口监听上游 proxy.golang.org 的 index 增量更新流,经 Kafka 分发至同步 Worker:

# 同步脚本关键逻辑(带幂等校验)
go run cmd/sync/main.go \
  --upstream=https://proxy.golang.org \
  --index-url=https://proxy.golang.org/index \
  --db-path=/data/rocksdb \
  --verify=true  # 启用 checksum 比对再写入

--verify=true 触发对 sum.golang.org 的实时哈希比对,避免中间人篡改;--db-path 指向持久化元数据的 LSM-tree 实例,支持高并发读与原子写入。

缓存一致性保障策略

层级 一致性机制 生效范围
CDN 边缘 ETag + Cache-Control:max-age=3600 模块 zip / go.mod
中心存储 RocksDB WriteBatch + WAL 元数据与校验和
客户端请求 If-None-Match 回源校验 强制验证变更
graph TD
  A[Client GET v1.2.3.zip] --> B{CDN Edge}
  B -->|ETag match| C[Return 304]
  B -->|Miss or Mismatch| D[中心集群]
  D -->|Read from RocksDB| E[校验 sum.golang.org]
  E -->|Pass| F[Set new ETag & Cache]
  E -->|Fail| G[Reject with 403]

3.2 线下Meetup标准化:技术议题筛选算法与地域化运营SOP落地

技术议题热度加权评分模型

采用多维信号融合策略,对候选议题打分:

def score_topic(topic: dict) -> float:
    # topic = {"relevance": 0.8, "novelty": 0.6, "local_demand": 0.9, "speaker_expertise": 0.7}
    return (
        topic["relevance"] * 0.4 +
        topic["novelty"] * 0.2 +
        topic["local_demand"] * 0.3 +
        topic["speaker_expertise"] * 0.1
    )  # 权重经A/B测试校准,本地需求权重最高

地域化SOP执行清单

  • ✅ 每城前置7天完成议题终审与讲师确认
  • ✅ 场地适配检查(含网络测速、投影兼容性)
  • ✅ 社群预热推送(按用户技术栈标签定向)

SOP执行效果对比(Q3数据)

指标 标准化前 标准化后
议题弃用率 32% 9%
平均到场率 61% 78%

流程协同机制

graph TD
    A[议题池] --> B{本地技术雷达扫描}
    B -->|匹配度≥0.75| C[自动进入待审队列]
    B -->|匹配度<0.75| D[转区域策展人人工复核]

3.3 教育体系下沉:高校Go语言课程共建中的编译原理教学重构

传统编译原理教学常依托C/Java等语言,抽象语法树(AST)构建与语义分析环节学生理解困难。Go语言标准库 go/ast 与 go/parser 提供轻量、可调试的实战接口,天然适配本科教学。

Go中AST构建的教学生态优势

  • 无需手动实现词法/语法分析器
  • go/parser.ParseFile 直接生成完整AST节点
  • 错误信息友好,支持源码定位

示例:解析并打印函数声明节点

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    src := `func Hello(name string) int { return len(name) }`
    fset := token.NewFileSet()
    file, err := parser.ParseFile(fset, "", src, 0)
    if err != nil {
        panic(err)
    }
    ast.Inspect(file, func(n ast.Node) bool {
        if fn, ok := n.(*ast.FuncDecl); ok {
            fmt.Printf("Func: %s, Params: %d\n", 
                fn.Name.Name, len(fn.Type.Params.List))
        }
        return true
    })
}

逻辑说明:parser.ParseFile 在内存中完成词法→语法分析,返回*ast.File;ast.Inspect 深度遍历所有节点;*ast.FuncDecl 是函数声明的AST类型,其 Name.Name 为标识符字符串,Params.List 是参数声明切片。fset 提供位置信息支持后续教学调试。

教学重构关键维度对比

维度 传统方案(Java/C) Go语言教学方案
AST构建成本 需手写Yacc/Bison或ANTLR 标准库一行ParseFile
调试可见性 中间表示抽象难追踪 ast.Print() 直接输出结构树
学生上手周期 4–6周
graph TD
    A[学生输入Go源码] --> B[go/parser.ParseFile]
    B --> C[生成ast.File AST根节点]
    C --> D[ast.Inspect遍历]
    D --> E[匹配FuncDecl/TypeSpec等教学重点节点]
    E --> F[可视化打印/简单语义检查]

第四章:从50万到100万:可持续增长的四大引擎构建

4.1 中文Go工具链国产化:gopls中文诊断增强与VS Code插件性能优化

中文诊断能力升级

gopls v0.14+ 新增 zh-CN 本地化诊断消息支持,通过 go env -w GOLANGORG_LANG=zh-CN 启用。核心增强点包括:

  • 错误码映射表动态加载(error_codes_zh.json)
  • 类型推导错误附带中文上下文示例
  • Go文档注释自动转译(基于 AST 节点语义对齐)

VS Code 插件关键优化项

优化维度 原实现耗时 优化后耗时 改进机制
诊断响应延迟 820ms 210ms LSP 请求批处理 + 缓存穿透过滤
中文符号索引构建 3.4s 1.1s 增量式 Unicode 分词器
// gopls/internal/lsp/source/diagnostics.go(节选)
func (s *Server) diagnose(ctx context.Context, uri span.URI) ([]*Diagnostic, error) {
    diags, err := s.baseDiagnose(ctx, uri)
    if err != nil {
        return nil, err
    }
    // 注入中文本地化处理器:仅当客户端声明 supportsLocalization == true
    return localizeDiagnostics(diags, s.cfg.Locale), nil // Locale = "zh-CN"
}

localizeDiagnostics 对 Diagnostic.Message 和 Diagnostic.Code 进行双层映射:先查错误码字典,再对模板参数(如 %v 类型名)执行中文类型名替换(如 []int → 整数切片),确保语义准确。

性能瓶颈突破路径

graph TD
    A[VS Code 发送 textDocument/diagnostic] --> B{gopls 是否命中缓存?}
    B -->|是| C[返回预渲染中文诊断]
    B -->|否| D[AST 遍历 + 类型检查]
    D --> E[调用 localizeDiagnostics]
    E --> F[Unicode-aware tokenization]
    F --> C

4.2 企业级案例沉淀机制:金融/政企场景Go微服务迁移路径图谱绘制

在金融与政企系统中,迁移非仅技术替换,更是合规性、可观测性与灰度能力的体系化重构。典型路径包含三阶段演进:存量系统解耦 → 流量分层迁移 → 全链路治理闭环。

核心迁移策略矩阵

维度 传统单体(Java) Go微服务适配方案
事务一致性 XA/JTA分布式事务 Saga + 补偿事务(TCC轻量封装)
配置治理 ZooKeeper+Spring Cloud Config Nacos+OpenConfig Schema校验
日志审计 Log4j+ELK定制解析 Zap+OpenTelemetry结构化日志

数据同步机制

// 增量同步适配器:兼容Oracle GoldenGate与MySQL Binlog
func NewSyncAdapter(srcDB, dstDB *sql.DB, schema string) *SyncAdapter {
    return &SyncAdapter{
        src:      srcDB,
        dst:      dstDB,
        schema:   schema,
        batchSize: 500, // 控制事务粒度,避免长事务阻塞金融核心库
        timeout:   30 * time.Second, // 防止跨中心网络抖动导致超时雪崩
    }
}

该适配器通过batchSize实现金融级事务可控性,timeout参数强制熔断异常链路,保障监管报送时效性。

graph TD
    A[存量Oracle核心账务] -->|OGG捕获| B(同步网关)
    B --> C{路由决策引擎}
    C -->|实时风控请求| D[Go风控服务]
    C -->|T+1报表| E[批处理Go作业]
    D --> F[全链路TraceID透传]
    E --> F

4.3 社区贡献者成长飞轮:从Issue响应者到SIG Maintainer的晋升通道设计

社区成长飞轮由三阶跃迁驱动:响应 → 主导 → 治理,每阶设明确准入标尺与反馈闭环。

贡献路径关键里程碑

  • ✅ Level 1(Issue响应者):累计关闭10+带good-first-issue标签的PR,需通过CI验证与2人CR
  • ✅ Level 2(SIG Contributor):主导完成1个子模块文档重构或测试覆盖率提升至85%+
  • ✅ Level 3(SIG Maintainer):发起并推动1次SIG技术提案(RFC),获≥75%成员投票通过

自动化晋升校验脚本(片段)

# 检查 contributor 是否满足 SIG Contributor 门槛
gh api "search/issues?q=repo:org/repo+author:$USER+label:documentation+updated:>=2024-01-01" \
  --jq '.total_count'  # 返回文档类issue参与数

逻辑说明:调用GitHub REST API检索用户近半年在指定仓库中参与documentation标签issue的数量;--jq '.total_count'提取匹配总数,作为自动化评审输入参数,避免人工统计偏差。

晋升能力矩阵

能力维度 Issue响应者 SIG Contributor SIG Maintainer
技术深度 理解单点修复 掌握模块边界 定义架构演进
协作影响力 响应他人PR 主导跨模块联调 主持SIG例会
graph TD
  A[响应Issue] -->|持续贡献+反馈| B[申请SIG Contributor]
  B -->|提交RFC+共识达成| C[SIG Maintainer]
  C -->|反哺新人指导| A

4.4 多模态知识分发:CLI交互式教程与WebAssembly在线沙箱实践

多模态知识分发突破传统文档边界,将 CLI 教程与 WASM 沙箱深度耦合,实现“所学即所用”。

交互式 CLI 教程驱动学习流

通过 tutor-cli 工具链解析 YAML 教程定义,动态生成可执行命令序列:

# tutorial.yaml 中定义的 step-2 示例
tutor run --step=2 --context=webgpu-demo
# 输出:自动注入 wasm-pack build + serve 指令并验证输出

逻辑分析:--step 指定原子化教学单元;--context 加载预置依赖上下文(如 rust-toolchain.toml、wasm-bindgen 版本约束),确保环境一致性。

WebAssembly 在线沙箱架构

沙箱基于 wasi-sdk 编译 + wasmtime 运行时,支持无插件实时执行:

组件 作用
wasm-pack Rust → WASM 构建与绑定
wasi-socket 沙箱内模拟网络 I/O
js-sys 浏览器 API 安全桥接
graph TD
    A[CLI 教程输入] --> B{解析 YAML 步骤}
    B --> C[注入 WASM 模块依赖]
    C --> D[沙箱内编译/运行]
    D --> E[实时输出与断言校验]

第五章:致所有正在写Go代码的人

写出可测试的函数,而非“能跑就行”的逻辑

在真实项目中,http.HandlerFunc 的匿名闭包常导致单元测试难以注入依赖。例如,直接在 handler 内部调用 database.QueryRow() 会使测试必须启动真实数据库。正确做法是将数据访问逻辑提取为接口:

type UserStore interface {
    GetUserByID(ctx context.Context, id int) (*User, error)
}

func NewUserHandler(store UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id, _ := strconv.Atoi(r.URL.Query().Get("id"))
        user, err := store.GetUserByID(r.Context(), id)
        // ...
    }
}

这样,测试时可传入 mockUserStore{} 实现,100% 覆盖业务分支,且无需 testcontainers 启动 PostgreSQL。

避免在 defer 中忽略错误

以下代码在生产环境曾引发静默数据丢失:

f, _ := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY, 0644)
defer f.Close() // Close() 可能返回 error,但被丢弃!
f.Write([]byte("request processed\n"))

应改为:

defer func() {
    if err := f.Close(); err != nil {
        log.Printf("failed to close log file: %v", err)
    }
}()

使用结构化日志替代 fmt.Println

某支付网关因日志无 traceID 和字段结构,导致 SRE 平均故障定位耗时达 23 分钟。切换至 zerolog 后:

场景 fmt.Printf 日志 zerolog 结构化日志
请求入口 2024-05-12 10:22:31 GET /pay 200 {"level":"info","time":"2024-05-12T10:22:31Z","method":"GET","path":"/pay","status":200,"trace_id":"abc123","duration_ms":142.7}
错误上下文 failed to validate card: invalid expiry {"level":"error","card_number_last4":"4242","expiry_month":12,"expiry_year":2025,"error":"invalid expiry"}

小心切片的底层数组共享陷阱

微服务间传递用户权限列表时,若直接 append(roles, "admin") 后返回,可能污染上游缓存中的原始切片。验证方式:

original := []string{"user", "reader"}
derived := append(original, "admin")
original[0] = "hacked" // 影响 derived?实测:否(因扩容触发新底层数组)
// 但若 original 容量足够:original := make([]string, 2, 4),则修改 original[0] 会同步影响 derived[0]

永远为 goroutine 设置超时或取消信号

一个未设 timeout 的 http.Get 在 DNS 故障时会永久阻塞 goroutine,最终耗尽 GOMAXPROCS 限制下的 P 资源。正确模式:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := http.DefaultClient.Do(req.WithContext(ctx))

Go module 版本兼容性实践

某团队升级 github.com/aws/aws-sdk-go-v2 至 v1.25.0 后,config.LoadDefaultConfig() 突然要求显式传入 config.WithRegion("us-east-1")。通过 go mod graph | grep aws-sdk-go-v2 发现间接依赖了旧版 SDK 的 github.com/aws/smithy-go,最终使用 replace 强制统一:

// go.mod
replace github.com/aws/smithy-go => github.com/aws/smithy-go v1.13.5

生产环境 panic 捕获需分层处理

HTTP handler 中 recover() 仅应捕获预期 panic(如 JSON 解析失败),而数据库连接中断等应由 sql.DB.PingContext() 健康检查提前暴露。实际部署中,我们为每个 HTTP handler 添加:

func withRecovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                if _, ok := r.(json.SyntaxError); ok {
                    http.Error(w, "invalid JSON", http.StatusBadRequest)
                    return
                }
                panic(r) // 其他 panic 仍向上抛出,触发全局监控告警
            }
        }()
        next.ServeHTTP(w, r)
    })
}

使用 pprof 定位真实瓶颈

某 API P99 延迟突增至 8s,go tool pprof -http=:8080 cpu.pprof 显示 67% 时间消耗在 runtime.mallocgc —— 追查发现 bytes.Repeat([]byte("0"), 1024*1024) 被高频调用。改用预分配缓冲池后,P99 下降至 127ms。

graph LR
A[HTTP Request] --> B[JSON Unmarshal]
B --> C[bytes.Repeat for padding]
C --> D[runtime.mallocgc]
D --> E[GC Pressure ↑]
E --> F[P99 Latency ↑]

扎根云原生,用代码构建可伸缩的云上系统。

发表回复

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