第一章:Go要求import路径形如“github.com/user/repo”的根本动因
Go语言强制要求导入路径必须是全局唯一、可解析的绝对路径,而非相对路径或本地别名。这一设计并非出于对特定代码托管平台的偏好,而是为实现其核心工程目标——可重现构建(reproducible builds)与零配置依赖解析。
Go模块系统的信任锚点
Go自1.11起将导入路径直接用作模块标识符(module path),该路径在go.mod中声明,并作为go get下载、校验和缓存的唯一依据。例如:
# 执行此命令时,Go工具链会:
# 1. 解析 "github.com/spf13/cobra" 为 HTTPS URL https://github.com/spf13/cobra
# 2. 按标准Git协议克隆仓库到 $GOPATH/pkg/mod/cache/download/
# 3. 根据 go.sum 中记录的校验和验证包完整性
go get github.com/spf13/cobra@v1.8.0
为什么不能使用相对路径或自定义别名?
- 相对路径(如
./utils)破坏模块可移植性:同一代码在不同机器或CI环境中路径语义不一致; - 别名机制(如
import mylib "local/utils")导致依赖图不可推导,go list -m all无法生成确定性模块树; - 缺失权威源会导致
go mod tidy无法自动发现和拉取间接依赖。
导入路径即协议+域名+路径的三元组
| 组成部分 | 示例 | 作用 |
|---|---|---|
| 协议隐含 | https://(默认) |
Go工具链自动补全,支持自定义GOPROXY或GONOSUMDB |
| 域名 | github.com |
区分不同托管服务(也支持 gitlab.com, gitee.com, 自建code.example.com) |
| 路径 | /user/repo |
对应仓库根目录,go.mod中module声明必须与此严格匹配 |
这种设计使Go摆脱了中心化包注册中心(如npm registry、PyPI),任何支持Git/HTTP的服务器均可成为模块源,同时保障了跨团队协作时依赖引用的确定性与可审计性。
第二章:Go toolchain的URL驱动包发现机制
2.1 import路径作为可解析URI的设计哲学与go list的实际解析流程
Go 的 import 路径并非简单字符串,而是遵循 RFC 3986 的可解析 URI:github.com/org/repo/sub/pkg 等价于 https://github.com/org/repo/sub/pkg(协议隐式为 https),支持重定向、代理和模块映射。
URI语义驱动的模块发现机制
go list首先将 import path 视为 authority+path 组合- 查询
GOPROXY(如https://proxy.golang.org)的/github.com/org/repo/@v/list端点 - 若失败,回退至
git ls-remote直接克隆元数据
实际解析流程(简化版)
# go list -f '{{.ImportPath}} {{.Dir}}' github.com/go-sql-driver/mysql
# 输出示例:
# github.com/go-sql-driver/mysql /home/user/go/pkg/mod/github.com/go-sql-driver/mysql@v1.7.1
此命令触发完整解析链:URI标准化 → proxy 检索 →
go.mod解析 → 本地缓存定位。-f模板中.Dir是经filepath.Clean()处理后的绝对路径,.ImportPath保持原始声明形式。
| 阶段 | 输入 | 输出类型 |
|---|---|---|
| URI标准化 | golang.org/x/net |
https://golang.org/x/net |
| Proxy查询 | @v/list |
版本列表文本 |
| 本地缓存解析 | @v/v1.19.0.info |
JSON元数据 |
graph TD
A[import \"rsc.io/quote\"] --> B[URI标准化]
B --> C[GOPROXY请求]
C --> D{响应成功?}
D -->|是| E[下载zip+go.mod]
D -->|否| F[git clone + go mod download]
E --> G[解压到GOCACHE]
F --> G
2.2 go get背后:vcs探测、元数据提取与go.mod生成的完整链路实操
go get 并非简单下载,而是一套自动化的模块发现与初始化流水线。
VCS 探测机制
Go 首先向目标路径(如 github.com/gorilla/mux)发起 HTTP HEAD 请求,检查 /.git/ 或 /go.mod 等端点,依据响应头 VCS 字段或 HTML <meta name="go-import"> 标签识别版本控制系统类型与代码根路径。
# 示例:手动触发元数据探测(Go 1.18+)
go get -d -v github.com/gorilla/mux@latest
-d跳过构建仅下载依赖;-v输出详细日志,可见Fetching https://github.com/gorilla/mux?go-get=1及后续重定向解析过程。
元数据提取与 go.mod 生成
若项目无 go.mod,Go 会自动初始化:
- 检测最高兼容 Go 版本(从
go.sum或源码中//go:build推断) - 创建最小
go.mod(含module、go指令及直接依赖)
| 步骤 | 输入 | 输出 |
|---|---|---|
| VCS 探测 | github.com/gorilla/mux |
git, https://github.com/gorilla/mux.git |
| 元数据抓取 | ?go-get=1 响应 |
import path, vcs, repo 三元组 |
| 模块初始化 | 无 go.mod 的仓库 |
自动生成 go.mod 并写入 require 条目 |
graph TD
A[go get github.com/x/y] --> B{HTTP GET ?go-get=1}
B --> C[解析 <meta name=“go-import”>]
C --> D[克隆 VCS 仓库]
D --> E[扫描根目录 go.mod]
E -->|缺失| F[自动生成 go.mod + go.sum]
E -->|存在| G[校验 checksum 并写入 require]
2.3 代理模式(GOPROXY)如何解耦源站依赖并实现跨域缓存一致性验证
GOPROXY 通过中间层拦截 go get 请求,将模块拉取从直连 sum.golang.org 和源站(如 GitHub)解耦,转而由可信代理统一调度与缓存。
缓存一致性保障机制
代理需同步校验 go.sum 签名与 index 元数据,确保跨地域节点返回相同模块哈希:
# 启用双重校验的 GOPROXY 配置
export GOPROXY="https://proxy.golang.org,direct"
export GOSUMDB="sum.golang.org"
此配置强制所有模块经 proxy 下载,但
GOSUMDB仍独立连接权威校验服务,实现「下载路径」与「完整性验证路径」分离,避免单点故障导致校验绕过。
跨域缓存同步策略
| 地域节点 | 同步触发条件 | 校验方式 |
|---|---|---|
| cn-proxy | 首次请求 + TTL 过期 | 实时回源 sum.golang.org |
| us-proxy | 模块元数据变更 webhook | 基于 X-Go-Mod-Checksum 对比 |
数据同步机制
graph TD
A[Client go get] --> B{GOPROXY}
B --> C[本地缓存命中?]
C -->|是| D[返回模块+校验头]
C -->|否| E[并发请求:① fetch module<br>② query sum.golang.org]
E --> F[双签名校验通过后写入缓存]
该流程确保任意节点响应前均完成跨域一致性断言,而非仅依赖本地缓存 TTL。
2.4 自定义域名导入(如 mycorp.com/pkg)的注册、重定向与go proxy兼容性实践
域名解析与 HTTPS 强制要求
Go 工具链默认要求 GOPROXY 下载路径必须通过 HTTPS,且 DNS 解析需指向可验证的 Web 服务器。mycorp.com/pkg 需配置 A/AAAA 记录并启用 TLS(推荐 Let’s Encrypt 自动续签)。
HTTP 重定向配置(Nginx 示例)
server {
listen 443 ssl;
server_name mycorp.com;
ssl_certificate /etc/letsencrypt/live/mycorp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mycorp.com/privkey.pem;
location /pkg/ {
# Go 模块发现协议要求:响应 200 + application/vnd.go+json
add_header Content-Type "application/vnd.go+json";
# 代理至内部模块服务或静态文件目录
alias /var/www/go-modules/pkg/;
}
}
逻辑说明:
/pkg/路径需支持GET /pkg/@v/list、/pkg/@v/v1.2.3.info等标准 Go Module Fetch 接口;alias确保路径映射精确,避免双斜杠问题;Content-Type是go list -m -json等命令解析元数据的关键标识。
go proxy 兼容性关键点
| 检查项 | 合规要求 | 验证命令 |
|---|---|---|
/.well-known/go-mod/v2 |
返回 {"version":"2"} |
curl -H 'Accept: application/json' https://mycorp.com/.well-known/go-mod/v2 |
@v/list 响应格式 |
每行一个语义化版本(如 v1.2.3\n) |
go list -m -versions mycorp.com/pkg |
| 重定向行为 | 禁止 302 跳转至非同域地址(违反 GOPROXY 安全策略) | curl -I https://mycorp.com/pkg/@v/list |
graph TD
A[go build] --> B{GOPROXY=https://proxy.golang.org}
B -->|fallback| C[mycorp.com/pkg@v1.2.3]
C --> D[DNS → IP]
D --> E[HTTPS GET /pkg/@v/v1.2.3.info]
E --> F[200 + JSON metadata]
2.5 离线构建场景下replace+replace-dir+GONOSUMDB的协同失效分析与修复方案
失效根源:三者语义冲突
replace 和 replace-dir 在离线环境中强制重写模块路径,但 GONOSUMDB=* 会跳过校验——导致 go build 仍尝试向 proxy 发起元数据请求(如 /@v/list),触发网络失败。
典型错误日志片段
go: github.com/example/lib@v1.2.3: reading https://proxy.golang.org/github.com/example/lib/@v/v1.2.3.info: 403 Forbidden
修复关键:双层隔离
- ✅ 设置
GOPROXY=off彻底禁用代理(比GONOSUMDB更底层) - ✅ 配合
GOSUMDB=off避免校验中断 - ❌ 单独设
GONOSUMDB=*不足以阻止 proxy 元数据探测
推荐离线构建环境变量组合
| 变量 | 值 | 作用 |
|---|---|---|
GOPROXY |
off |
禁用所有模块获取网络路径 |
GOSUMDB |
off |
关闭校验(含本地 replace) |
GOFLAGS |
-mod=mod |
强制使用 go.mod 状态 |
# 执行前务必导出(顺序无关,但需全部生效)
export GOPROXY=off GOSUMDB=off GOFLAGS="-mod=mod"
go build -o app ./cmd
此配置使
replace/replace-dir的路径重写真正落地,不再触发任何外部 HTTP 调用。
第三章:去中心化签名验证的基础设施支撑
3.1 Go checksum database(sum.golang.org)的Merkle树结构与客户端校验逻辑剖析
Go 的 checksum database 采用深度为 1 的扁平化 Merkle 树,所有模块校验和按字典序排序后两两哈希合并,最终生成单一 root。客户端校验时不仅验证目标模块哈希,还需获取对应 inclusion proof(路径哈希序列)以重构根。
Merkle 路径验证示例
// verifyInclusion checks if leafHash is in the tree with given root and proof
func verifyInclusion(leafHash, root []byte, proof [][]byte) bool {
h := leafHash
for _, sib := range proof {
if bytes.Compare(h, sib) < 0 {
h = sha256.Sum256(append(h, sib...)).[:] // left + right
} else {
h = sha256.Sum256(append(sib, h...)).[:] // right + left
}
}
return bytes.Equal(h, root)
}
proof是从叶节点到根的兄弟节点哈希列表;leafHash为module@version sum的 SHA256;顺序决定拼接方向(小值在左),确保确定性。
客户端校验关键步骤
- 请求
https://sum.golang.org/lookup/<module>@<version>获取sum和proves字段 - 下载对应
/.tree端点获取当前树高与根哈希 - 验证 inclusion proof 与签名证书链(由
trusted.golang.org签发)
| 组件 | 作用 | 来源 |
|---|---|---|
root |
全局一致性锚点 | /.tree 响应 |
proves |
Merkle 路径哈希数组 | lookup/ 响应 |
sig |
root + timestamp 的 Ed25519 签名 | 同上 |
graph TD
A[go get] --> B[fetch sum.golang.org/lookup]
B --> C{verify inclusion proof}
C --> D[recompute root]
D --> E[check sig against trusted cert]
3.2 GOPRIVATE与GOSUMDB配合实现私有模块零信任签名绕过的安全边界实践
Go 模块生态默认强制校验公共模块的 sum.golang.org 签名,但私有模块需明确划出信任边界,避免误触校验导致拉取失败或中间人风险。
安全边界定义策略
通过环境变量组合实现精准隔离:
GOPRIVATE=git.example.com/internal/*:声明匹配路径为私有,跳过代理与校验GOSUMDB=off或GOSUMDB=sum.golang.org+insecure:仅对GOPRIVATE范围内模块禁用校验
# 推荐生产级配置(最小权限原则)
export GOPRIVATE="git.corp.company.com/*,github.com/myorg/private-*"
export GOSUMDB="sum.golang.org" # 保持公共模块强校验
此配置下,
go get git.corp.company.com/internal/auth绕过GOSUMDB校验,而go get github.com/go-yaml/yaml仍受完整签名验证保护。
配置生效逻辑流程
graph TD
A[go get pkg] --> B{pkg path matches GOPRIVATE?}
B -->|Yes| C[Skip GOSUMDB lookup & signature check]
B -->|No| D[Query sum.golang.org + verify signature]
C --> E[Fetch directly from VCS]
D --> F[Fail on mismatch]
关键参数说明表
| 变量 | 值示例 | 作用 |
|---|---|---|
GOPRIVATE |
*.corp.com,github.com/myteam/* |
正则匹配私有域名/路径,触发“零校验”豁免 |
GOSUMDB |
sum.golang.org+insecure |
仅当 GOPRIVATE 匹配时,才启用该非安全模式 |
3.3 模块签名篡改检测:从go mod verify到go.sum双哈希比对的逐层验证实验
Go 模块完整性依赖 go.sum 文件中记录的双重哈希(h1: 主哈希 + go:sum 隐式校验),而 go mod verify 是其顶层验证入口。
验证流程可视化
graph TD
A[go build] --> B{go.sum 存在?}
B -->|是| C[解析模块路径与hash]
B -->|否| D[报错:missing go.sum]
C --> E[下载模块源码]
E --> F[计算 h1:SHA256+GOOS/GOARCH 归一化哈希]
F --> G[比对 go.sum 中对应条目]
手动触发验证
# 强制重新校验所有依赖哈希一致性
go mod verify
# 输出示例:all modules verified
该命令不联网,仅比对本地 go.sum 与 $GOPATH/pkg/mod/cache/download/ 中已缓存模块的实际哈希值,参数无须额外指定,隐式启用 GOSUMDB=off 时仍校验本地一致性。
go.sum 条目结构
| 模块路径 | 版本 | 哈希类型 | 哈希值(截断) |
|---|---|---|---|
| golang.org/x/net | v0.25.0 | h1: | a1b2c3...d4e5f6 |
| h1: | x7y8z9...0a1b2c |
双哈希设计防止单点哈希碰撞,提升供应链攻击检测鲁棒性。
第四章:跨域可重现构建的工程化保障体系
4.1 go mod download的确定性fetch行为与vendor目录生成中URL路径的语义锚定作用
go mod download 在模块解析阶段严格依据 go.sum 中记录的校验和与 go.mod 中声明的版本(如 v1.12.0)执行可重现的 fetch,不依赖本地缓存或网络重定向歧义。
URL路径作为语义锚点
模块导入路径(如 github.com/gorilla/mux)不仅是定位标识,更在 vendor/ 目录结构中固化为文件系统路径根,确保 go build -mod=vendor 时符号解析与下载时完全一致。
# 示例:显式触发确定性下载并验证路径锚定
go mod download github.com/gorilla/mux@v1.8.0
# 输出路径语义:$GOPATH/pkg/mod/cache/download/github.com/gorilla/mux/@v/v1.8.0.info
该命令强制从校验和可信源拉取,.info 文件包含原始 zip URL 和 go.mod URL,构成双重语义锚定。
关键保障机制
| 维度 | 作用 |
|---|---|
go.sum |
锁定内容哈希,防止供应链篡改 |
| URL路径结构 | 约束 vendor 目录层级,避免路径混淆 |
graph TD
A[go.mod 声明] --> B[go mod download]
B --> C[校验 go.sum]
C --> D[解压至 vendor/github.com/gorilla/mux]
D --> E[编译时路径解析匹配]
4.2 多版本共存时import路径唯一性如何约束module path versioning与go.work协同机制
Go 的 import 路径本质是 module path,而非文件路径。当多版本共存时(如 rsc.io/quote/v3 与 rsc.io/quote/v4),module path 必须显式包含语义化版本后缀,否则无法满足 Go 模块唯一性约束。
module path versioning 规则
- 主版本 v1 可省略
/v1,但 v2+ 必须显式声明 - 同一主版本下,不同次版本(如 v3.1.0 与 v3.2.0)共享同一 module path:
rsc.io/quote/v3
go.work 的协同作用
// go.work
use (
./cmd/v3
./cmd/v4
)
replace rsc.io/quote/v3 => ../quote/v3
replace rsc.io/quote/v4 => ../quote/v4
此配置使工作区同时加载两个不兼容主版本。
go build依据各子模块的go.mod中module rsc.io/quote/v3声明,结合go.work的replace和use,在构建图中为每个版本建立独立的 import namespace,从而保证import "rsc.io/quote/v3"与import "rsc.io/quote/v4"指向完全隔离的符号空间。
| 场景 | module path 合法性 | 原因 |
|---|---|---|
module example.com/lib |
✅ v1 默认隐式 | 不需 /v1 |
module example.com/lib/v2 |
✅ 显式 v2+ | 符合 SemVer 约束 |
module example.com/lib/v1 |
❌ 禁止 | v1 后缀冗余且破坏兼容性 |
graph TD
A[go build] --> B{解析 import path}
B --> C[rsc.io/quote/v3 → 查 go.work replace]
B --> D[rsc.io/quote/v4 → 查 go.work replace]
C --> E[加载 ./quote/v3/go.mod]
D --> F[加载 ./quote/v4/go.mod]
E & F --> G[独立符号表 + 无路径冲突]
4.3 构建锁(go.sum + go.mod + go.work)三元组在CI/CD流水线中的跨平台哈希对齐实践
在多架构CI环境中(如 linux/amd64、linux/arm64、darwin/arm64),go.sum 的哈希一致性易受构建环境差异影响。关键在于确保 go.mod(依赖声明)、go.sum(校验和快照)与 go.work(多模块工作区)三者哈希协同锁定。
三元组对齐核心策略
- 使用
GOOS=GOARCH= GO111MODULE=on GOPROXY=https://proxy.golang.org,direct统一构建上下文 - CI中强制执行
go mod tidy -e && go mod verify,失败即中断 go.work需显式声明use ./module-a ./module-b,避免隐式路径解析偏差
跨平台哈希校验代码示例
# 在CI job末尾统一校验三元组完整性
find . -name "go.mod" -o -name "go.sum" -o -name "go.work" | \
xargs sha256sum | sort | sha256sum | cut -d' ' -f1
该命令生成三元组内容的归一化摘要:先对各文件路径排序后计算SHA256,再对结果二次哈希。确保不同平台输出完全一致——任何换行符、BOM或空格差异都将导致哈希漂移。
| 环境变量 | 必需值 | 作用说明 |
|---|---|---|
GOSUMDB |
sum.golang.org 或 off |
控制校验和数据库验证行为 |
GOWORK |
显式指向 ./go.work |
避免工作区自动发现歧义 |
CGO_ENABLED |
(纯静态构建) |
消除C工具链引入的平台噪声 |
graph TD
A[CI Job Start] --> B[设置GOOS/GOARCH/GOPROXY]
B --> C[go work use ./...]
C --> D[go mod tidy && go mod verify]
D --> E{go.sum 哈希是否稳定?}
E -->|是| F[生成归一化指纹]
E -->|否| G[Fail: 依赖污染或缓存污染]
4.4 镜像同步工具(如 Athens、JFrog Go Registry)对原始URL路径的保真转发与重写策略对比
数据同步机制
Athens 默认采用路径透传(path-passthrough),原始 https://proxy.golang.org/github.com/org/repo/@v/v1.2.3.info 请求被原样转发至上游;JFrog Go Registry 则默认启用前缀重写,将 /go/ 路径段剥离后映射为内部存储路径。
配置差异对比
| 工具 | 默认路径行为 | 可配置性 | 示例重写规则 |
|---|---|---|---|
| Athens | 保真转发 | 低 | GO_PROXY=https://athens.example.com → 直接代理 |
| JFrog Go Registry | /go/github.com/... → github.com/... |
高 | artifactory.repo.path.prefix: /go |
# JFrog Artifactory go.repo 配置片段
repoType: go
key: go-remote
url: https://proxy.golang.org
repoLayoutRef: go-default
# 启用路径剥离:/go/ 前缀不参与上游请求构造
该配置使 Artifactory 在构造上游 URL 时自动截断 /go/,确保 GET /go/github.com/a/b/@v/v1.0.0.info 转发为 GET /github.com/a/b/@v/v1.0.0.info。
路径处理流程
graph TD
A[客户端请求] -->|/go/github.com/x/y@v/v1.0.0.mod| B(JFrog Go Registry)
B --> C{是否匹配 repoLayoutRef?}
C -->|是| D[剥离 /go/ 前缀]
D --> E[构造上游 URL]
C -->|否| F[透传原始路径]
第五章:超越URL:Go模块生态演进的未来挑战与收敛趋势
Go 1.11 引入模块(module)机制后,go.mod 文件取代 GOPATH 成为依赖管理的事实标准。然而,随着云原生、微服务和多语言协同开发的深入,模块生态正面临一系列超出传统 URL 导入路径的结构性挑战。
模块代理失效场景下的可信构建保障
在金融与政务类私有云环境中,企业常禁用外部代理(如 proxy.golang.org),强制使用内部镜像。但当某模块发布者意外删除 GitHub 仓库(如 github.com/legacy-lib/utils v0.3.2 tag 被撤回),且未同步至内部 Nexus Repository 时,go build 将因 checksum mismatch 失败。真实案例:某省级医保平台在 CI 流水线中遭遇该问题,最终通过 go mod edit -replace 硬编码 SHA256 校验和并签署 .mod 文件哈希至 HashiCorp Vault 实现可信回滚。
多版本共存引发的语义冲突
Kubernetes 生态中,k8s.io/client-go v0.26.x 与 v0.28.x 同时被不同 Operator 依赖,导致 runtime.Scheme 注册逻辑不兼容。Go 模块虽支持 replace 和 exclude,但无法自动解析类型别名(如 metav1.Time 在 v0.26 中为 time.Time 别名,v0.28 中新增 DeepCopy 方法)。团队采用 gofork 工具生成隔离包路径,并在 go.mod 中显式声明:
replace k8s.io/client-go => ./vendor/client-go-v0.26
replace k8s.io/client-go => ./vendor/client-go-v0.28 // 错误!Go 不允许多重 replace 同一模块
实际方案是构建双模块工作区(workspace mode),配合 GOWORK=off 环境变量分阶段构建。
模块校验机制的落地瓶颈
| 场景 | 标准行为 | 企业实践痛点 |
|---|---|---|
go mod verify |
校验本地缓存模块哈希 | 缓存被污染后无法定位原始来源 |
go sumdb.sum.golang.org |
全局透明日志 | 内网无法访问,且日志条目不可审计 |
go mod download -json |
输出模块元数据 | JSON 中缺失 SBOM 字段(如 SPDX ID、许可证 SPDX 表达式) |
某头部云厂商已将 go.sum 解析为 CycloneDX BOM,并集成至 Sigstore Cosign 签名流水线,实现模块级 cosign sign --oidc-issuer https://auth.enterprise.id --oidc-client-id go-mod-signer。
构建可重现性的跨工具链断层
Bazel + Gazelle 与 go build -mod=readonly 对 //go:embed 的处理差异导致二进制哈希漂移;Nixpkgs 中 buildGoModule 默认启用 -trimpath,而企业 CI 要求保留绝对路径用于 debuginfo 符号映射。解决方案是在 nixpkgs 中 patch go 原生构建器,添加 --keep-absolute-paths 标志,并通过 go mod vendor 预固化所有嵌入文件到 vendor/ 目录。
模块发现协议的标准化真空
当前 go list -m -json all 仅输出静态依赖图,无法反映运行时动态加载(如 plugin.Open() 加载的 .so 模块)或 WebAssembly 导入(wazero 运行时加载的 github.com/wasm-examples/math)。CNCF Sandbox 项目 modindex 正在定义基于 HTTP+JSON-LD 的模块发现端点规范,其草案已在 Istio 1.21 的扩展模块注册中心中试点部署。
模块签名密钥轮换周期从 3 年压缩至 90 天,要求 go get 支持 X.509 OCSP Stapling 验证;go mod graph 输出需原生支持 Graphviz DOT 与 Mermaid 兼容格式——以下为某微服务网关模块依赖的 Mermaid 可视化示意:
graph TD
A[api-gateway/v2] --> B[authn-module@v1.4.0]
A --> C[rate-limit-core@v0.9.3]
C --> D[redis-driver@v2.1.0+incompatible]
B --> E[jwt-go@v3.2.0+incompatible]
E -. deprecated .-> F[github.com/dgrijalva/jwt-go] 