Posted in

Go上传私服必须关闭的3个默认开关:GOPRIVATE、GOSUMDB、GOINSECURE配置失效真相曝光

第一章:Go上传私服必须关闭的3个默认开关:GOPRIVATE、GOSUMDB、GOINSECURE配置失效真相曝光

Go模块代理生态中,GOPRIVATEGOSUMDBGOINSECURE 三者协同控制私有模块的解析、校验与传输安全策略。但大量开发者在配置私服(如 JFrog Artifactory、Nexus Repository)时遭遇“明明设置了 GOPRIVATE 却仍向 proxy.golang.org 请求校验”或“go get 私有包失败:x509 certificate signed by unknown authority”等问题——根源并非配置遗漏,而是三者存在隐式依赖与优先级冲突。

GOPRIVATE 决定模块是否跳过公共代理

该环境变量声明匹配模式(支持通配符),仅当模块路径匹配时,Go 才禁用 GOPROXYGOSUMDB 的默认行为。注意:它本身不启用不安全通信,也不绕过 TLS 验证

# ✅ 正确示例:匹配所有 company.com 域下模块
export GOPRIVATE="*.company.com"

# ❌ 错误示例:未加通配符导致子路径不生效
export GOPRIVATE="company.com"  # company.com/internal 不被识别

GOSUMDB 必须显式禁用或重定向

即使 GOPRIVATE 已设置,Go 仍默认向 sum.golang.org 请求校验和——若私服未托管校验和数据库,此请求必然失败。必须同步关闭:

# 方案1:完全禁用校验和验证(仅限可信内网)
export GOSUMDB=off

# 方案2:指向私服提供的 sumdb 兼容服务(如 Artifactory 的 Go Virtual Repo)
export GOSUMDB=sum.company.com

GOINSECURE 仅对 HTTP 私服生效,且需与 GOPRIVATE 对齐

若私服使用 HTTP(非 HTTPS),GOINSECURE 必须包含与 GOPRIVATE 完全一致的域名模式,否则 TLS 跳过不触发:

环境变量 正确值 错误值 后果
GOPRIVATE *.company.com company.com 子路径模块仍走公共代理
GOINSECURE *.company.com company.com HTTP 私服连接被 TLS 拦截
GOSUMDB off 或自定义地址 未设置 校验和请求超时/404

执行验证命令确认生效:

go env GOPRIVATE GOSUMDB GOINSECURE  # 检查当前值
go list -m all 2>&1 | grep -i "proxy\|sum"  # 观察是否跳过公共服务

第二章:GOPRIVATE机制深度解析与绕过陷阱

2.1 GOPRIVATE环境变量的语义边界与通配符匹配原理

GOPRIVATE 定义 Go 模块代理的“私有豁免域”,其值为以逗号分隔的模块路径前缀列表,*不支持正则表达式,仅支持简单通配符 ``(且仅在末尾)**。

匹配规则核心

  • example.com/internal/* → ✅ 匹配 example.com/internal/apiexample.com/internal/v2/util
  • example.com/* → ✅ 匹配 example.com/foo/bar,但 ❌ 不匹配 sub.example.com/mod
  • *.example.com → ❌ 无效(* 不能出现在开头或中间)

通配符语义边界示例

# 正确设置:豁免两个组织下的所有模块
export GOPRIVATE="gitlab.company.com/*,github.com/my-team/*"

逻辑分析:Go 工具链对每个前缀执行 前缀匹配 + 星号展开gitlab.company.com/* 表示“以该字符串为完整前缀的模块路径”,* 仅作为通配占位符,不参与 glob 或正则解析;参数 GOPRIVATE 值被按 , 分割后逐项比对模块导入路径。

输入模块路径 匹配 github.com/my-team/* 原因
github.com/my-team/cli 完全符合前缀+任意后缀
github.com/my-team/v2/sdk * 覆盖 /v2/sdk
github.com/other-team/lib 前缀不一致
graph TD
    A[go get github.com/my-team/cli] --> B{GOPRIVATE 包含 my-team/*?}
    B -->|是| C[跳过 proxy.sum.golang.org 校验]
    B -->|否| D[走公共校验流程]

2.2 私服域名未被正确识别的典型场景复现(含go mod download日志分析)

常见触发场景

  • GOPROXY 设置为 https://goproxy.io,direct,但私服地址(如 https://proxy.example.com)未显式包含在代理链中
  • go.modreplace 指向私有模块时,未同步配置 GOPRIVATE=example.com
  • DNS 解析正常,但 Go 工具链因 GONOSUMDB 缺失而拒绝校验私有域名

日志关键线索

执行 go mod download -x github.com/example/internal@v1.2.0 时输出:

# github.com/example/internal
cd /tmp/gopath/pkg/mod/cache/vcs/3f4a2b1c...; git -c log.showsignature=false log -n1 --format=format:%H %ct %D refs/tags/v1.2.0
# get https://proxy.golang.org/github.com/example/internal/@v/v1.2.0.info
# get https://proxy.golang.org/github.com/example/internal/@v/v1.2.0.info: 404 Not Found

此日志表明:Go 默认回退至 proxy.golang.org未尝试访问私服域名。根本原因是 GOPRIVATE 未覆盖 example.com,导致 Go 认为其为公开模块,跳过私有代理。

配置修复对照表

环境变量 错误值 正确值 效果
GOPROXY https://goproxy.io,direct https://proxy.example.com,https://goproxy.io,direct 强制优先走私服
GOPRIVATE 未设置 example.com 禁用校验并启用代理

校验流程(mermaid)

graph TD
    A[go mod download] --> B{GOPRIVATE 匹配模块域名?}
    B -->|否| C[走 GOPROXY 默认链:proxy.golang.org]
    B -->|是| D[跳过校验,使用 GOPROXY 中首个匹配私服]
    D --> E[成功拉取 v1.2.0.info]

2.3 GOPRIVATE与GO111MODULE=on协同失效的调试实验(对比go env输出差异)

GOPRIVATE 未正确匹配私有域名时,即使 GO111MODULE=on,Go 仍会尝试向公共代理(如 proxy.golang.org)解析私有模块,导致 go get 失败。

环境变量对比关键项

变量 GO111MODULE=on + 正确 GOPRIVATE GO111MODULE=on + 错误 GOPRIVATE(如 git.corp
GOPROXY https://proxy.golang.org,direct 同左,但 direct 分支被错误跳过
GOPRIVATE git.corp.example.com git.corp不匹配 git.corp.example.com

失效复现命令

# 错误配置(子域名通配不生效)
export GOPRIVATE="git.corp"
export GO111MODULE=on
go env -w GOPRIVATE="git.corp"  # 注意:此写法不支持 glob,仅精确前缀匹配

🔍 GOPRIVATE前缀匹配,非通配符或正则;git.corp 无法匹配 git.corp.example.com,必须写为 git.corp.example.comcorp.example.com

调试流程

graph TD
    A[执行 go get private/mod] --> B{GOPRIVATE 是否匹配导入路径?}
    B -->|是| C[跳过 GOPROXY,直连 VCS]
    B -->|否| D[尝试 proxy.golang.org → 404/timeout]

2.4 多级子域名配置疏漏导致模块回退至proxy.golang.org的真实案例

某企业私有 Go 代理服务部署于 goproxy.internal.corp,但构建流水线中 GOPROXY 环境变量误设为 https://goproxy.internal.corp(缺失 /sumdb/proxy 路径后缀),且未配置 GOSUMDB=off 或自定义 sumdb。

根本原因分析

Go 工具链在解析 goproxy.internal.corp 时,因缺少路径前缀,将请求默认转发至 https://goproxy.internal.corp/<module>/@v/list —— 该路径返回 404 后,自动降级至 proxy.golang.org

关键配置对比

配置项 正确值 错误值
GOPROXY https://goproxy.internal.corp/proxy,https://proxy.golang.org https://goproxy.internal.corp
GOSUMDB sum.goproxy.internal.corp 未设置(触发默认 sum.golang.org
# 构建脚本中错误的环境变量设置(已修复)
export GOPROXY="https://goproxy.internal.corp"  # ❌ 缺失 /proxy 路径
export GOSUMDB=""                               # ❌ 触发默认校验源

逻辑分析:Go 1.13+ 的 proxy 协议要求 GOPROXY 中每个 endpoint 必须以 /proxy/ 结尾(后者由工具链补全 /proxy)。此处无尾斜杠且无路径,Go 将其视作根域,后续所有模块请求均构造为 GET /<path>,而反向代理未配置通配路由规则,直接 404 → 降级。

graph TD
    A[go build] --> B{GOPROXY = goproxy.internal.corp?}
    B -->|无路径| C[GET /github.com/foo/bar/@v/list]
    C --> D[404 Not Found]
    D --> E[自动回退 proxy.golang.org]

2.5 实战:通过go list -m all验证GOPRIVATE生效状态并定位跳过私有模块原因

验证 GOPRIVATE 是否被 Go 工具链识别

执行以下命令查看环境变量解析结果:

go env GOPRIVATE

若输出为空或未包含目标域名(如 git.example.com),说明环境未正确配置,需运行 go env -w GOPRIVATE=git.example.com

检查模块加载行为

运行核心诊断命令:

go list -m -json all 2>&1 | jq 'select(.Replace != null or .Error != null) | {Path, Version, Error}'

逻辑分析go list -m all 强制解析所有依赖模块的元信息;-json 输出结构化数据便于过滤;jq 筛出含 .Replace(代理重写)或 .Error(拉取失败)的条目,精准定位跳过原因(如 invalid version: unknown revision 表明私有模块未走私有源)。

常见跳过原因对照表

现象 根本原因 修复动作
module git.example.com/internal/util: reading git.example.com/internal/util/go.mod: 404 Not Found GOPRIVATE 未覆盖子路径 扩展为 git.example.com/*
verifying git.example.com/internal/util@v1.2.3: checksum mismatch GOPROXY 缓存污染 go clean -modcache + GOPROXY=direct 重试

调试流程图

graph TD
    A[执行 go list -m all] --> B{是否报错?}
    B -->|是| C[检查 .Error 字段]
    B -->|否| D[检查是否缺失私有模块条目]
    C --> E[确认 GOPRIVATE 域名匹配]
    D --> E
    E --> F[验证 git/ssh 认证连通性]

第三章:GOSUMDB校验拦截与私服签名绕过策略

3.1 sum.golang.org工作流程与GOSUMDB=off/sum.golang.org的区别本质

Go 模块校验依赖于模块签名数据库(sum.golang.org),其核心是透明日志(Trillian)保障不可篡改性。

数据同步机制

客户端首次拉取模块时,向 sum.golang.org 查询哈希记录,并验证 Merkle inclusion proof:

# 示例:go get 自动触发校验
GOSUMDB=sum.golang.org go get github.com/gorilla/mux@v1.8.0

→ Go 工具链向 https://sum.golang.org/lookup/github.com/gorilla/mux@v1.8.0 发起 GET 请求,返回含 h1: 前缀的 SHA256 校验和及对应 inclusion proof;随后本地验证该 proof 是否属于可信日志根。

关键区别本质

配置 网络请求 校验强度 可信锚点
GOSUMDB=sum.golang.org ✅ 同步查询 + proof 验证 强(分布式共识日志) Go 官方日志根证书
GOSUMDB=off ❌ 完全跳过校验

流程对比

graph TD
    A[go get] --> B{GOSUMDB 设置}
    B -->|sum.golang.org| C[查询日志+验证proof]
    B -->|off| D[仅比对本地go.sum]

3.2 私服无sumdb签名时go get触发404 checksum mismatch的完整链路追踪

当私有 Go 代理(如 Athens、JFrog Go)未启用 sum.golang.org 兼容的 sumdb 签名服务时,go get 会陷入校验闭环失败:

请求链路关键节点

  • go get 首先向私服请求 @v/list@v/v1.2.3.info
  • 继而发起 /sum 查询(如 https://goproxy.example.com/github.com/user/repo/@v/v1.2.3.mod
  • 若私服返回 404(非 200),客户端 fallback 到 sum.golang.org —— 但该域名被拦截或不可达,最终触发 checksum mismatch

校验失败核心逻辑

# go get -v github.com/private/repo@v1.2.3
go: downloading github.com/private/repo v1.2.3
verifying github.com/private/repo@v1.2.3: 
  github.com/private/repo@v1.2.3: reading https://sum.golang.org/lookup/github.com/private/repo@v1.2.3: 404 Not Found

此处 404 并非模块不存在,而是 sumdb 未收录(因私服未同步签名),Go 客户端误判为校验数据缺失,拒绝缓存。

依赖校验状态映射表

状态码 来源 客户端行为
200 私服 /sum 直接校验,跳过 sum.golang.org
404 私服 /sum 强制回退 sum.golang.org → 失败
503 sum.golang.org 触发 GOINSECURE 降级策略
graph TD
  A[go get] --> B{私服支持 /sum?}
  B -- 是 --> C[返回校验和 → 验证通过]
  B -- 否 --> D[404 → 回退 sum.golang.org]
  D --> E{sum.golang.org 可达?}
  E -- 否 --> F[checksum mismatch error]

3.3 GOSUMDB=direct在模块拉取阶段的精确作用点及风险权衡

模块拉取流程中的关键拦截点

GOSUMDB=directgo getgo buildsumdb 验证环节被立即生效,跳过向 sum.golang.org 请求校验和比对,直接信任本地缓存或远程模块服务器返回的 go.sum 条目。

行为对比表

场景 默认行为 GOSUMDB=direct
校验和来源 远程 sumdb + 本地 go.sum 双校验 仅信任本地 go.sum(若存在)或完全跳过校验
中间人攻击防护 强(签名可验证) 完全失效
私有模块兼容性 常失败(无公共 sumdb 条目) 无缝支持
# 示例:强制绕过 sumdb 验证拉取私有模块
GOSUMDB=direct go get git.example.com/internal/lib@v1.2.3

此命令跳过 sum.golang.org 查询,不校验 git.example.com 模块的哈希一致性;若 go.sum 缺失或被篡改,将静默接受恶意代码。

风险权衡本质

  • ✅ 解决私有仓库/离线环境拉取阻塞
  • ❌ 放弃 Go Module 最核心的供应链完整性保障
graph TD
    A[go get] --> B{GOSUMDB=direct?}
    B -- 是 --> C[跳过 sum.golang.org 请求]
    B -- 否 --> D[查询 sumdb + 校验哈希]
    C --> E[仅依赖本地 go.sum 或接受未经验证的校验和]

第四章:GOINSECURE配置的局限性与HTTPS降级实践误区

4.1 GOINSECURE仅作用于go get/go mod download阶段的协议层校验,不覆盖HTTP客户端重定向逻辑

GOINSECURE 的设计边界常被误解:它仅在模块获取的初始 TLS 握手与证书验证环节生效,对后续 HTTP 重定向链路完全无感知。

重定向流程中的校验盲区

# 示例:go get 会先校验 https://insecure.example.com,
# 但若该站 302 重定向至 http://malicious.site,则后者绕过 GOINSECURE
GOINSECURE=insecure.example.com go get insecure.example.com@v1.0.0

此命令中,GOINSECURE 仅豁免 insecure.example.com 的 HTTPS 证书校验;若其返回 Location: http://evil.com/...,Go 的 net/http 客户端将直接发起明文 HTTP 请求,且不检查 GOINSECURE 是否涵盖 evil.com——因重定向已脱离模块发现上下文。

关键行为对比

阶段 受 GOINSECURE 控制? 原因
go get 初始域名 TLS 模块路径解析后主动校验
301/302 目标地址请求 由标准 HTTP 客户端处理,无模块语义
graph TD
    A[go get module] --> B{解析 module path}
    B --> C[发起 HTTPS 请求]
    C --> D[检查 GOINSECURE 匹配?]
    D -->|是| E[跳过证书验证]
    D -->|否| F[拒绝连接]
    C --> G[收到 302 Redirect]
    G --> H[用 net/http.Client 重发请求]
    H --> I[完全忽略 GOINSECURE]

4.2 自签名证书私服下GOINSECURE未生效的抓包分析(Wireshark+curl对比go命令行为)

现象复现

设置 GOPROXY=https://my-registry.localGOINSECURE=my-registry.local 后,go mod download 仍报 x509: certificate signed by unknown authority

curl vs go 的 TLS 行为差异

工具 是否校验证书链 是否受 GOINSECURE 影响 底层库
curl -k ❌(跳过) ❌(无关) OpenSSL/libcurl
go get ✅(强制) ✅(但仅作用于 GOPROXY 域名匹配) net/http + crypto/tls

关键代码验证

# 查看 go 实际请求的 Host 头(注意:go 会重写 Host 为 proxy 域名,但证书验证仍基于原始 registry)
go env -w GOPROXY="https://my-registry.local"
go env -w GOINSECURE="my-registry.local"  # ✅ 此处必须与证书 CN/SAN 完全一致(不含端口、协议)

GOINSECURE 仅豁免 TLS 证书验证,不绕过 DNS 或 HTTP 重定向;若证书中 SAN 为 *.local 而实际访问 my-registry.local:8443,则因端口不参与匹配,仍失败。

Wireshark 抓包关键线索

graph TD
    A[go mod download] --> B{TLS ClientHello}
    B --> C[ServerHello + Certificate]
    C --> D{go tls.Config.InsecureSkipVerify?}
    D -->|false| E[panic: x509 error]
    D -->|true| F[继续握手]

核心原因:GOINSECURE 仅在 net/http.Transport 初始化时注入 InsecureSkipVerify=true,但要求域名精确匹配且不带端口

4.3 混合网络环境(内网HTTP+公网HTTPS)中GOINSECURE通配符匹配失败的调试方法

GOPROXY 指向混合源(如 https://proxy.golang.org,direct),而内网模块路径为 git.internal.corp/foo/bar 时,GOINSECURE=*.internal.corp 可能失效——因 Go 1.21+ 默认仅对 模块路径前缀 进行通配匹配,而非域名。

匹配规则验证

# 查看当前生效的 insecure 模块列表(需 Go 1.22+)
go env -w GOINSECURE="*.corp,git.internal.corp"
go list -m -f '{{.Path}} {{.Version}}' git.internal.corp/foo/bar

逻辑分析:GOINSECURE 值以逗号分隔,每个条目必须精确匹配模块路径开头(非 URL 主机名)。*.internal.corp 不匹配 git.internal.corp/...,因通配符不支持子域前缀;应改用 git.internal.corpinternal.corp(若模块路径以该字符串开头)。

常见配置对照表

配置值 是否匹配 git.internal.corp/lib 说明
*.internal.corp 通配符仅作用于路径,非域名
git.internal.corp 精确前缀匹配
internal.corp ✅(若模块路径为 internal.corp/x 要求路径严格以此开头

调试流程

graph TD
    A[执行 go get] --> B{是否报错 x509 certificate signed by unknown authority}
    B -->|是| C[检查 GOPROXY 和 GOSUMDB]
    C --> D[运行 go env GOINSECURE]
    D --> E[验证值是否为模块路径前缀,而非 DNS 域名]

4.4 实战:结合net/http.Transport自定义DialTLS绕过证书校验的临时替代方案(非推荐但可验证)

为什么需要临时绕过 TLS 校验?

在开发联调、内网测试或对接自签名证书服务时,x509: certificate signed by unknown authority 错误会阻断 HTTP 客户端流程。此时可临时定制 http.TransportDialTLS,但需明确:此举完全放弃证书链验证,绝不适用于生产环境

自定义 DialTLS 的核心逻辑

transport := &http.Transport{
    DialTLS: func(network, addr string) (net.Conn, error) {
        conn, err := tls.Dial(network, addr, &tls.Config{
            InsecureSkipVerify: true, // ⚠️ 关键:跳过证书验证
        })
        return conn, err
    },
}
  • InsecureSkipVerify: true 禁用服务端证书签名、域名匹配、有效期等全部校验;
  • tls.Dial 直接建立 TLS 连接,绕过 http.DefaultTransport 默认的 crypto/tls 验证路径;
  • 此配置不干扰 HTTP/2 协商,但会丢失证书指纹、OCSP 装订等安全上下文。

安全风险对照表

风险项 启用 InsecureSkipVerify 默认行为
中间人攻击防护 ❌ 失效 ✅ 强制校验
域名匹配(SNI) ❌ 跳过 ✅ 校验 CN/SAN
证书过期检查 ❌ 忽略 ✅ 拒绝过期证书

推荐演进路径

  • ✅ 优先导入根 CA 到 tls.Config.RootCAs
  • ✅ 使用 tls.Config.VerifyPeerCertificate 实现白名单校验
  • ❌ 永远避免在 CI/CD 或容器镜像中保留 InsecureSkipVerify: true

第五章:总结与展望

核心技术栈的生产验证

在某省级政务云平台迁移项目中,我们基于本系列实践构建的 Kubernetes 多集群联邦架构已稳定运行 14 个月。集群平均可用率达 99.992%,跨 AZ 故障自动切换耗时控制在 8.3 秒内(SLA 要求 ≤15 秒)。关键指标如下表所示:

指标项 实测值 SLA 要求 达标状态
API Server P99 延迟 127ms ≤200ms
日志采集丢包率 0.0017% ≤0.01%
Helm Release 回滚成功率 99.98% ≥99.9%

真实故障复盘:etcd 存储碎片化事件

2024 年 3 月,某金融客户集群因高频 ConfigMap 更新(日均 12,800+ 次)导致 etcd 后端存储碎片率达 63%。我们紧急启用 etcdctl defrag + --compact-revision 组合操作,并同步将 ConfigMap 生命周期管理纳入 GitOps 流水线(Argo CD v2.9.2),通过预检脚本自动拦截单次提交超 50 个 ConfigMap 的 PR。修复后碎片率降至 4.2%,且后续 97 天零复发。

# 生产环境 etcd 碎片检测脚本核心逻辑
ETCD_ENDPOINTS="https://etcd-01:2379,https://etcd-02:2379"
FRAGMENTATION=$(etcdctl --endpoints=$ETCD_ENDPOINTS endpoint status \
  --write-out=json | jq -r '.[0].Status.FragmentationPercentage')
if (( $(echo "$FRAGMENTATION > 40" | bc -l) )); then
  echo "ALERT: Fragmentation $FRAGMENTATION% exceeds threshold"
  # 触发自动 compact & defrag pipeline
fi

运维效能提升量化结果

采用本方案后,某电商中台团队的发布效率发生结构性变化:

  • 单服务部署耗时从平均 18 分钟压缩至 210 秒(含安全扫描与灰度验证)
  • 生产环境配置错误率下降 76%(对比 2023 年基线数据)
  • SRE 工程师日均人工干预次数由 14.2 次降至 2.8 次

该成效源于将 Istio 服务网格策略、OPA 准入控制、以及 Prometheus 告警抑制规则全部编码为可版本化的 YAML 清单,并通过 FluxCD v2 实现秒级同步。

未来演进路径

随着 eBPF 技术在可观测性领域的成熟,我们已在测试环境部署 Cilium Hubble 作为替代方案,初步验证其网络流日志采集性能较传统 sidecar 模式提升 3.8 倍。同时,针对边缘场景的轻量化需求,正基于 K3s 构建“云边协同”双模架构,其中边缘节点通过 MQTT over WebAssembly 实现低带宽下的遥测上报,首期试点已在 37 个工厂网关完成部署。

graph LR
A[边缘设备] -->|MQTT/WASM| B(Cilium Agent)
B --> C{K3s Edge Cluster}
C -->|gRPC Stream| D[中心云 Cilium Cluster]
D --> E[(Hubble UI Dashboard)]
E --> F[AI 异常检测模型]
F --> G[自动生成 NetworkPolicy]

社区协作新范式

当前已有 12 家企业将本方案中的 GitOps 策略模板贡献至 CNCF Landscape 的 “Infrastructure as Code” 分类,其中 3 个模板被 HashiCorp Terraform Registry 官方收录为 Verified Module。最新版策略引擎已支持动态加载 OPA Bundle 的 OCI 镜像,使合规检查规则更新周期从小时级缩短至秒级。

技术债务治理实践

在遗留系统容器化过程中,我们发现 63% 的 Java 应用存在 -Xmx 与容器内存限制不匹配问题。为此开发了自动化检测工具 jvm-cgroup-validator,它通过读取 /sys/fs/cgroup/memory/memory.limit_in_bytes 与 JVM 运行时参数比对,在 CI 阶段强制阻断不合规镜像构建。上线半年内,因内存溢出导致的 Pod 驱逐事件归零。

可持续交付能力边界

当微服务数量突破 217 个时,现有 Argo CD 同步机制出现明显延迟(P95 同步耗时达 42 秒)。我们正在验证基于 Kyverno 的实时策略注入方案,通过监听 Kubernetes Event 直接触发资源校验,初步压测显示在 500 服务规模下事件处理吞吐量达 18,400 EPS,较原方案提升 5.2 倍。

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

发表回复

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