Posted in

Go语言脚本必须掌握的11个net/http黑科技:无需框架实现Webhook接收器、健康检查端点与Prometheus指标暴露

第一章:Go语言net/http基础与脚手化设计哲学

Go 语言将 HTTP 服务内建为标准库核心能力,net/http 包以极简接口抽象网络交互:一个 Handler 接口(仅需实现 ServeHTTP(http.ResponseWriter, *http.Request) 方法)和一个轻量 Server 结构体,共同构成可组合、无框架依赖的服务基石。这种设计拒绝隐式约定,强调显式控制——路由、中间件、错误处理全部由开发者按需组装,而非交由框架调度。

核心组件解耦模型

  • http.ResponseWriter:抽象响应写入器,屏蔽底层 TCP 连接细节,支持状态码、Header 设置与 Body 流式写入
  • *http.Request:封装完整请求上下文,含 URL、Method、Header、Body、Form 数据及上下文(Context)字段
  • HandlerHandlerFunc:统一处理契约,后者通过类型转换将函数转为接口,实现“函数即服务”

快速启动一个脚本化 HTTP 服务

以下代码可在单文件中直接运行,无需构建或依赖管理:

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    // 定义处理函数:返回当前时间戳
    http.HandleFunc("/time", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        fmt.Fprintf(w, "Current time: %s", time.Now().Format(time.RFC3339))
    })

    // 启动服务器,监听 localhost:8080
    log.Println("Starting server on :8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

执行方式:保存为 server.go,终端运行 go run server.go;随后访问 http://localhost:8080/time 即得响应。该模式天然适配 DevOps 脚本场景——服务逻辑即代码,部署即执行,无编译产物、无配置文件、无生命周期管理胶水代码。

设计哲学体现

特性 表现
零魔法 所有行为可见可控,无自动扫描、无反射路由注册
组合优先 通过 http.Handler 链式包装实现日志、认证等中间件
面向小任务 单文件可承载完整服务,适合 CLI 工具嵌入 HTTP 管理端点

这种“脚本化”并非牺牲工程性,而是将复杂性下沉为可复用的 Handler 组合单元,让每个 HTTP 服务都可如 shell 脚本般轻量启动、快速验证、即写即用。

第二章:Webhook接收器的零依赖实现

2.1 HTTP请求解析与签名验证的密码学实践

HTTP签名机制是保障API调用完整性与身份可信的核心手段,其本质是将请求元数据(方法、路径、时间戳、Body哈希等)按约定顺序拼接后,使用私钥生成HMAC-SHA256或RSA-PSS签名。

签名构造关键字段

  • X-Signature: Base64编码的签名值
  • X-Timestamp: 请求发起毫秒级Unix时间戳(防重放)
  • X-Nonce: 一次性随机字符串(协同防重放)

HMAC-SHA256签名示例(Python)

import hmac, hashlib, base64

def sign_request(method, path, timestamp, nonce, body_hash, secret_key):
    # 拼接规范字符串:METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY_HASH
    msg = f"{method}\n{path}\n{timestamp}\n{nonce}\n{body_hash}".encode()
    sig = hmac.new(secret_key.encode(), msg, hashlib.sha256).digest()
    return base64.b64encode(sig).decode()

# 示例调用
signature = sign_request("POST", "/v1/order", "1718234567890", "a1b2c3", "sha256=abc...", "my_secret")

逻辑分析msg严格遵循大小写敏感、换行分隔的标准化序列;secret_key为服务端共享密钥;body_hash推荐采用sha256=前缀格式化摘要,确保可扩展性。

签名验证流程(Mermaid)

graph TD
    A[接收HTTP请求] --> B[提取X-Timestamp/X-Nonce/X-Signature]
    B --> C[校验时间戳±5分钟有效性]
    C --> D[检查Nonce是否已使用]
    D --> E[按相同规则重构签名原文]
    E --> F[HMAC比对签名值]
    F -->|一致| G[放行请求]
    F -->|不一致| H[拒绝并返回401]

2.2 幂等性保障与事件去重的内存+时间窗口策略

在高并发事件驱动架构中,重复消息是常态。仅依赖数据库唯一索引无法应对毫秒级重放,需结合内存缓存与滑动时间窗口实现低延迟去重。

核心设计思想

  • 内存层(如 ConcurrentHashMap<String, Long>)暂存事件ID与接收时间戳
  • 时间窗口设为 5 分钟(可配置),超出即自动驱逐
  • 双校验:先查内存命中,再对过期ID做异步落库归档

去重逻辑代码示例

public boolean isDuplicate(String eventId) {
    long now = System.currentTimeMillis();
    Long receivedAt = idCache.get(eventId); // 内存O(1)查询
    if (receivedAt != null && now - receivedAt <= 300_000) {
        return true; // 5分钟内已存在
    }
    idCache.put(eventId, now); // 写入新事件
    return false;
}

idCache 使用 ConcurrentHashMap 避免锁竞争;300_000 单位为毫秒,对应5分钟窗口;put() 同时触发LRU淘汰(需配合定时清理线程或ExpiringMap)。

策略对比表

维度 纯DB去重 内存+时间窗口
延迟 ~10–50ms ~0.1–2ms
存储压力 高(全量写) 低(仅活跃ID)
容灾一致性 最终一致(需补偿)
graph TD
    A[事件到达] --> B{内存查ID}
    B -->|存在且未超时| C[标记重复/丢弃]
    B -->|不存在或已超时| D[写入内存+记录时间]
    D --> E[异步归档至DB]

2.3 JSON Schema动态校验与结构化错误响应构建

核心校验流程

采用 ajv@8 实现运行时 Schema 加载与验证,支持 $ref 远程引用与关键字扩展。

const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(userSchema); // userSchema 来自配置中心动态拉取

if (!validate(userData)) {
  return buildStructuredError(validate.errors); // 错误归一化入口
}

allErrors: true 确保收集全部违规项;strict: false 兼容非标准 Schema 扩展;validate.errors 为 AJV 原生错误数组,含 instancePathschemaPathmessage 等字段。

结构化错误响应设计

统一转换为三层语义结构:

字段 类型 说明
code string 业务错误码(如 VALIDATION.MISSING_FIELD
path string JSON Pointer 路径(如 /email
detail string 可读提示(支持 i18n 占位符)

错误映射逻辑

graph TD
  A[原始 AJV Error] --> B{是否 required?}
  B -->|是| C[code=VALIDATION.REQUIRED]
  B -->|否| D[匹配 keyword → code 映射表]
  D --> E[注入 path + localized detail]

2.4 异步处理队列集成(channel+worker池)与背压控制

核心设计思想

采用 Go channel 作为任务分发总线,配合固定大小的 worker 池实现并发可控的异步执行;通过带缓冲的 input channel 与 worker 内部 select 配合 default 分支实现轻量级背压。

背压控制机制

当输入通道满载时,新任务被拒绝而非阻塞,保障系统稳定性:

// inputCh 缓冲区大小 = worker 数 × 2,防止单点积压
inputCh := make(chan Task, numWorkers*2)

// worker 中非阻塞接收
func worker(id int, jobs <-chan Task) {
    for {
        select {
        case job := <-jobs:
            process(job)
        default:
            time.Sleep(10 * time.Millisecond) // 退避探测
        }
    }
}

逻辑分析:default 分支使 worker 主动让出调度权,避免忙等;缓冲区尺寸依据吞吐压测确定,兼顾延迟与内存开销。

策略对比表

策略 吞吐波动容忍 OOM风险 实现复杂度
无缓冲channel 极低
动态扩容buffer
固定缓冲+reject
graph TD
    A[Producer] -->|send with select| B[inputCh]
    B --> C{Worker Pool}
    C --> D[Process]
    C -.->|backpressure via full buffer| A

2.5 TLS双向认证与Webhook端点的生产级安全加固

为什么单向TLS不够?

现代Webhook场景中,仅服务端验证客户端(单向TLS)无法防止恶意第三方伪造事件推送。双向认证(mTLS)强制双方交换并校验证书,构建零信任通信基线。

mTLS握手流程

graph TD
    A[Webhook Client] -->|1. ClientHello + client cert| B[Webhook Server]
    B -->|2. ServerHello + server cert + CA chain| A
    A -->|3. CertificateVerify + encrypted handshake| B
    B -->|4. Finished| A

Nginx配置关键片段

# 启用mTLS并严格校验
ssl_client_certificate /etc/ssl/certs/webhook-ca.pem;  # 根CA公钥
ssl_verify_client on;                                   # 强制验证客户端证书
ssl_verify_depth 2;                                     # 允许中间CA层级

ssl_client_certificate 指定可信根CA证书链,ssl_verify_client on 触发双向握手;ssl_verify_depth 2 支持“Root → Intermediate → Leaf”三级证书结构,兼顾安全性与运维灵活性。

安全策略对照表

策略项 基础部署 生产加固
证书有效期检查
OCSP Stapling
请求头签名验证
IP白名单+证书绑定

第三章:健康检查端点的多维度可观测设计

3.1 Liveness/Readiness探针的语义化状态建模与自动路由注册

Kubernetes 原生探针仅返回布尔值,无法表达服务内部多维健康语义。为此,我们引入状态机建模:将 Readiness 映射为 (TrafficReady, ConfigLoaded, DependencyHealthy) 三元组,Liveness 映射为 (ProcessAlive, MemoryStable, GCNotStuck)

状态语义定义

  • TrafficReady=true:已通过蓝绿流量预检,可接收新请求
  • ConfigLoaded=partial:核心配置加载完成,非关键配置可异步补全
  • DependencyHealthy 支持分级:critical(DB)、soft(缓存)、optional(日志服务)

自动路由注册逻辑

# service.yaml 中声明语义化探针
livenessProbe:
  httpGet:
    path: /healthz?format=stateful  # 返回 JSON 状态机快照
    port: 8080

该端点返回 {"liveness":{"process":true,"gc":"ok"},"readiness":{"traffic":true,"deps":{"db":"up","cache":"degraded"}}}。Ingress Controller 解析此结构,动态更新 Envoy 路由权重——例如 cache: degraded 时自动降权 30% 流量至备用集群。

状态组合 路由行为 SLA 影响
traffic:true, deps.db:up 全量流量 + 优先级最高 ✅ 99.99%
traffic:true, deps.cache:degraded 主链路+缓存降权分流 ⚠️ 99.9%
traffic:false 从服务发现中摘除,不入路由表 ❌ 0%
graph TD
  A[HTTP /healthz?format=stateful] --> B{解析JSON状态}
  B --> C[提取readiness.traffic]
  B --> D[聚合deps健康分值]
  C & D --> E[计算路由权重向量]
  E --> F[调用xDS API推送Envoy]

3.2 依赖服务连通性探测(DB、Redis、HTTP外部依赖)的超时熔断实践

探测策略分层设计

  • 快速失败:对 DB 连接池初始化阶段执行 ping() + timeout=2s
  • 周期探活:Redis 使用 INFO server 每 15s 一次,失败连续 3 次触发熔断;
  • HTTP 依赖:通过健康端点 /actuator/health 调用,超时设为 1.5s,重试 1 次。

熔断器配置示例(Resilience4j)

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)           // 错误率阈值
    .waitDurationInOpenState(Duration.ofSeconds(60))  // 熔断保持时间
    .ringBufferSizeInHalfOpenState(10)  // 半开态试探请求数
    .build();

逻辑分析:当 DB/Redis/HTTP 在滑动窗口(默认 100 次调用)中错误率达 50%,立即跳闸;60 秒后进入半开态,仅放行 10 次请求验证恢复能力。参数 ringBufferSizeInHalfOpenState 防止雪崩式试探。

依赖类型 探测方式 超时阈值 熔断触发条件
MySQL connection.isValid(3000) 3s 连续 5 次连接超时
Redis redis.ping() 1s 15s 内 3 次失败
HTTP API GET /health 1.5s 2 分钟内错误率 ≥60%
graph TD
    A[发起依赖调用] --> B{是否在熔断状态?}
    B -- 是 --> C[返回降级响应]
    B -- 否 --> D[执行带超时的探测]
    D --> E{成功?}
    E -- 否 --> F[记录失败,更新熔断统计]
    E -- 是 --> G[正常返回]

3.3 健康状态聚合与结构化JSON输出的标准化协议适配

健康状态聚合需统一多源指标(如CPU、内存、服务连通性、依赖延迟),并映射至跨平台可解析的JSON Schema。

数据建模规范

定义核心字段:

  • status(string,枚举:"healthy"/"degraded"/"unavailable"
  • timestamp(ISO 8601 UTC)
  • components(对象数组,含namestatelatency_ms

JSON Schema 示例

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["status", "timestamp", "components"],
  "properties": {
    "status": { "enum": ["healthy", "degraded", "unavailable"] },
    "timestamp": { "format": "date-time" },
    "components": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name", "state"],
        "properties": {
          "name": { "type": "string" },
          "state": { "type": "string" }
        }
      }
    }
  }
}

该Schema强制校验字段存在性、枚举值及时间格式,保障下游系统(如Prometheus Alertmanager、Kubernetes readiness probe)能无歧义解析。

协议适配层职责

  • 将Zabbix告警、Nagios状态码、OpenTelemetry metrics统一转换为上述Schema;
  • 自动补全缺失字段(如缺latency_ms则设为null);
  • 签名字段x-health-signature用于防篡改。
源系统 映射关键字段 转换策略
Prometheus up == 1"healthy" 布尔→枚举
Spring Boot Actuator statusstatus 直接透传,标准化大小写
graph TD
  A[原始指标流] --> B{协议适配器}
  B --> C[字段标准化]
  B --> D[空值填充]
  B --> E[Schema验证]
  C --> F[结构化JSON]
  D --> F
  E --> F

第四章:Prometheus指标暴露的轻量级原生集成

4.1 自定义Gauge/Counter/Histogram指标的运行时注册与生命周期管理

Prometheus客户端库支持在应用运行中动态注册指标,避免启动时全量声明带来的耦合与资源浪费。

动态注册示例(Go)

// 创建可复用的Counter实例(非全局单例)
reqCounter := promauto.With(reg).NewCounter(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests",
        ConstLabels: prometheus.Labels{"service": "api-gateway"},
    },
)
// 后续可安全调用 reqCounter.Inc()

promauto.With(reg) 绑定自定义 Registry 实例,确保指标归属明确;ConstLabels 在注册时固化维度,避免运行时重复标注开销。

生命周期关键约束

  • ✅ 指标对象注册后不可迁移 Registry
  • ❌ 同名指标重复注册将 panic(需提前检查 reg.Gather() 或使用 MustRegister 的幂等变体)
  • ⚠️ Gauge 值应由持有方自主更新,不依赖注册时机
指标类型 是否支持 Reset 典型使用场景
Counter 请求计数、错误累计
Gauge 是(设为0) 当前并发数、内存用量
Histogram 请求延迟分布
graph TD
    A[创建指标实例] --> B{是否已注册?}
    B -->|否| C[调用 reg.MustRegister]
    B -->|是| D[直接更新值]
    C --> E[指标进入活跃生命周期]
    D --> E

4.2 HTTP中间件式指标采集:请求延迟、状态码分布与并发连接数监控

HTTP中间件是实现无侵入式可观测性的理想切面。在请求生命周期中注入指标采集逻辑,可同时捕获三类核心信号。

核心指标维度

  • 请求延迟:以 http_request_duration_seconds(直方图)记录处理耗时
  • 状态码分布:按 status_codemethod 多维标签统计计数器
  • 并发连接数:使用 gauge 类型实时跟踪活跃连接(http_connections_active

中间件实现示例(Go)

func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 记录并发连接数变化
        httpConnectionsActive.Inc() 
        defer httpConnectionsActive.Dec()

        // 包装响应写入器以捕获状态码
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(rw, r)

        // 上报延迟与状态码
        httpReqDuration.WithLabelValues(r.Method, r.URL.Path).Observe(time.Since(start).Seconds())
        httpReqCount.WithLabelValues(strconv.Itoa(rw.statusCode), r.Method).Inc()
    })
}

httpConnectionsActiveprometheus.Gauge 类型,Inc()/Dec() 原子增减;httpReqDuration 使用预设分位桶(如 0.01,0.1,0.2,0.5,1,2,5),支持 SLI 计算;WithLabelValues 动态绑定路由与方法维度,避免指标爆炸。

指标关联性示意

指标名 类型 关键标签 用途
http_request_duration_seconds_bucket Histogram le, method, path P95延迟分析
http_requests_total Counter code, method 错误率归因
http_connections_active Gauge 容量水位预警
graph TD
    A[HTTP Request] --> B[Middleware Enter]
    B --> C[并发计数+1]
    C --> D[记录起始时间]
    D --> E[执行业务Handler]
    E --> F[捕获响应状态码]
    F --> G[上报延迟+状态码+并发-1]

4.3 指标标签(label)的动态注入与业务上下文绑定技巧

在微服务调用链中,静态 label 无法反映实时业务语义。需将请求路径、租户 ID、订单状态等上下文动态注入 Prometheus 指标。

标签动态注入示例(OpenTelemetry + Prometheus)

# 使用 OpenTelemetry 的 BoundInstrument
from opentelemetry.metrics import get_meter
meter = get_meter("payment.service")
counter = meter.create_counter("payment.processed")

# 动态绑定业务上下文标签
counter.add(1, {
    "tenant_id": context.get("tenant_id", "unknown"),  # 来自 JWT 或 Header
    "order_status": order.status.name,
    "region": request.headers.get("X-Region", "global")
})

逻辑分析:counter.add() 的第二参数为 label 字典;所有 key 必须预注册(避免 cardinality 爆炸),tenant_idorder_status 均来自运行时上下文,非硬编码。

常见业务上下文映射表

上下文来源 推荐 label 键 安全约束
JWT payload tenant_id 需白名单校验
HTTP Header client_type 仅允许 mobile/web/api
DB 查询结果 payment_method 枚举值预定义(避免泄漏)

数据同步机制

graph TD
    A[HTTP Request] --> B{Extract Context}
    B --> C[Inject into Span & Metrics]
    C --> D[Prometheus Exporter]
    D --> E[Relabeling Rule]
    E --> F[Final Label Set]

Relabeling 在 Prometheus server 端进一步过滤/重写 label,保障指标一致性与安全性。

4.4 /metrics端点的细粒度权限控制与文本格式生成性能优化

权限策略分层设计

采用 RBAC + 标签路由双校验机制:

  • metrics:cpurole:monitoring + label:prod
  • metrics:jvm:gcrole:devops + scope:cluster

文本格式生成优化

避免重复序列化,引入缓存感知的 TextFormat.write004() 调用链:

// 使用预分配缓冲区与线程局部 StringBuilder
private static final ThreadLocal<StringBuilder> TL_SB = 
    ThreadLocal.withInitial(() -> new StringBuilder(8192));

public void writeMetrics(Writer writer, CollectorRegistry registry) {
    StringBuilder sb = TL_SB.get().setLength(0); // 复用内存
    TextFormat.write004(sb, registry.metricFamilySamples()); // 直接写入sb
    writer.write(sb.toString()); // 一次flush
}

逻辑分析:StringBuilder.setLength(0) 避免对象重建开销;TextFormat.write004() 是 Prometheus Java Client 中专为文本协议 v0.0.4 优化的无锁写入方法,参数 registry.metricFamilySamples() 按 Family 分组预排序,减少迭代跳转。

性能对比(单位:ms,10K samples)

方式 GC 次数 平均耗时 内存分配
原生 write004(writer) 12 48.3 14.2 MB
缓冲复用优化版 2 11.7 2.1 MB
graph TD
    A[/metrics 请求] --> B{鉴权网关}
    B -->|标签匹配失败| C[403 Forbidden]
    B -->|RBAC 通过| D[缓存命中?]
    D -->|是| E[返回 LRU 缓存文本]
    D -->|否| F[调用 write004 + TL_SB]
    F --> G[写入响应并缓存]

第五章:完整可运行脚本工程化封装与部署指南

项目结构标准化设计

一个可维护的脚本工程必须具备清晰的目录骨架。典型结构如下:

my_script_tool/  
├── src/  
│   ├── __init__.py  
│   ├── core.py          # 主业务逻辑  
│   └── utils.py         # 辅助函数(日志、配置解析等)  
├── config/  
│   ├── default.yaml     # 默认配置模板  
│   └── prod.yaml        # 生产环境覆盖配置  
├── tests/  
│   └── test_core.py     # pytest 兼容测试用例  
├── scripts/  
│   └── run_pipeline.sh  # 启动入口 Shell 封装  
├── pyproject.toml     # 现代 Python 构建元数据(含依赖、打包指令)  
└── Dockerfile         # 容器化部署基础镜像定义  

配置驱动的环境适配机制

使用 PyYAML + pydantic-settings 实现强类型配置管理。src/core.py 中通过 Settings() 自动加载 config/prod.yaml 或环境变量覆盖项,支持 ENV=staging python -m src.core 动态切换上下文。

可复现的依赖与版本锁定

pyproject.toml 中声明构建系统与依赖约束:

[build-system]  
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]  
build-backend = "setuptools.build_meta"  

[project]  
name = "my-script-tool"  
version = "0.3.1"  
dependencies = [  
  "requests>=2.28.0,<3.0.0",  
  "pydantic-settings>=2.2.1",  
  "structlog>=23.2.0"  
]  

打包发布全流程自动化

执行以下命令即可生成跨平台可执行文件与 PyPI 包:

# 生成源码分发包与 wheel  
python -m build  

# 推送至私有仓库(如 Nexus)  
twine upload --repository nexus dist/*  

容器化部署实践

Dockerfile 采用多阶段构建降低镜像体积:

FROM python:3.11-slim AS builder  
WORKDIR /app  
COPY pyproject.toml .  
RUN pip install build && python -m build --wheel  

FROM python:3.11-slim  
WORKDIR /app  
COPY --from=builder /app/dist/*.whl .  
RUN pip install *.whl && rm *.whl  
COPY config/prod.yaml /app/config.yaml  
CMD ["my-script-tool", "--config", "/app/config.yaml"]  

CI/CD 流水线集成示意

使用 GitHub Actions 实现提交即验证:

on: [push, pull_request]  
jobs:  
  test:  
    runs-on: ubuntu-latest  
    steps:  
      - uses: actions/checkout@v4  
      - name: Set up Python  
        uses: actions/setup-python@v4  
        with: { python-version: '3.11' }  
      - run: pip install pytest && pytest tests/  
  build-and-push:  
    if: github.event_name == 'push' && startsWith(github.head_ref, 'release/')  
    # ... 触发镜像构建与推送逻辑  

监控与可观测性嵌入

src/core.py 初始化时注入结构化日志与指标埋点:

import structlog  
from prometheus_client import Counter  

logger = structlog.get_logger()  
EXECUTION_COUNT = Counter('script_executions_total', 'Total script runs')  

def main():  
    EXECUTION_COUNT.inc()  
    logger.info("pipeline.started", version="0.3.1", env="prod")  
    # ... 实际业务逻辑  

多环境部署策略对比

环境 启动方式 配置来源 日志输出目标
开发 python -m src.core config/default.yaml stdout + file
测试 docker run ... 挂载 config/test.yaml stdout + Loki
生产 Kubernetes Job ConfigMap + Secret stdout → Fluentd → ES

错误处理与降级保障

所有外部调用(HTTP、数据库连接)均配置重试策略与熔断器:

from tenacity import retry, stop_after_attempt, wait_exponential  
from circuitbreaker import circuit  

@circuit(failure_threshold=5, recovery_timeout=60)  
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))  
def fetch_remote_data(url):  
    return requests.get(url, timeout=15).json()  

专攻高并发场景,挑战百万连接与低延迟极限。

发表回复

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