第一章:Go语言最好课程终极验证:从零构建高可用订单服务
构建一个真实可用的订单服务,是检验Go语言工程能力的黄金标尺——它要求并发安全、数据一致性、可观测性与弹性容错缺一不可。本章将从零开始,用纯Go标准库与轻量生态(仅依赖database/sql、net/http、sync、context)实现一个支持高并发下单、幂等处理、本地事务回滚及健康检查的订单服务。
项目初始化与模块结构
mkdir order-service && cd order-service
go mod init github.com/yourname/order-service
创建清晰分层目录:
cmd/server/main.go—— 入口与HTTP服务启动internal/order/—— 领域核心(含Order结构体、Repository接口)internal/storage/—— SQLite实现(便于本地验证,后续可无缝替换为PostgreSQL)pkg/middleware/—— 日志、超时、恢复中间件
订单创建的并发安全实现
关键在于避免超卖与重复下单。使用sync.Map缓存近期请求ID(TTL 5分钟),结合数据库唯一约束双重校验:
// internal/order/service.go
func (s *Service) CreateOrder(ctx context.Context, req CreateOrderRequest) (Order, error) {
idempotencyKey := req.IdempotencyKey
if s.idempotencyCache.Load(idempotencyKey) != nil {
return Order{}, errors.New("duplicate request")
}
s.idempotencyCache.Store(idempotencyKey, struct{}{}) // 写入缓存
tx, err := s.db.BeginTx(ctx, nil)
if err != nil { return Order{}, err }
defer tx.Rollback() // 自动回滚,除非显式Commit
// 执行插入(SQL中定义idempotency_key UNIQUE)
_, err = tx.ExecContext(ctx, "INSERT INTO orders (idempotency_key, product_id, amount) VALUES (?, ?, ?)",
idempotencyKey, req.ProductID, req.Amount)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
return Order{}, errors.New("order already exists")
}
return Order{}, err
}
if err := tx.Commit(); err != nil {
return Order{}, err
}
return Order{ID: generateID(), IdempotencyKey: idempotencyKey}, nil
}
健康检查与就绪探针
暴露/healthz(存活)与/readyz(依赖就绪)端点,后者校验数据库连接:
| 端点 | 响应码 | 触发条件 |
|---|---|---|
/healthz |
200 | 进程存活 |
/readyz |
200 | 数据库SELECT 1成功 |
/readyz |
503 | 数据库连接超时或查询失败 |
服务启动后,可通过curl http://localhost:8080/readyz即时验证部署完整性。
第二章:Go语言核心能力筑基与工程化实践
2.1 Go模块管理与多版本依赖治理实战
Go 模块(Go Modules)是 Go 1.11 引入的官方依赖管理系统,彻底替代了 $GOPATH 时代的手动管理方式。核心在于 go.mod 文件声明模块路径与依赖约束。
初始化与版本锁定
go mod init example.com/myapp
go mod tidy # 自动下载、解析并写入 go.sum
go mod tidy 扫描源码导入路径,拉取最小必要版本,写入 go.mod 的 require 块,并生成加密校验的 go.sum,确保构建可重现。
多版本共存机制
Go 支持同一依赖不同主版本并存(如 github.com/gorilla/mux v1.8.0 与 v2.0.0+incompatible),通过 语义化导入路径 区分:
import (
"github.com/gorilla/mux" // v1.x
muxv2 "github.com/gorilla/mux/v2" // v2+
)
模块路径末尾 /v2 是 Go 的版本标识约定,非目录结构。
替换与升级策略
| 场景 | 命令示例 | 说明 |
|---|---|---|
| 临时替换本地开发版 | go mod edit -replace old=../local |
绕过远程拉取,加速调试 |
| 升级到兼容最新补丁 | go get github.com/some/lib@latest |
仅升 patch,保持 minor 不变 |
graph TD
A[go build] --> B{解析 import}
B --> C[查 go.mod require]
C --> D[匹配版本/替换规则]
D --> E[校验 go.sum]
E --> F[构建成功]
2.2 并发模型深度解析:goroutine、channel与worker pool工业级实现
goroutine:轻量级并发原语
单个 goroutine 仅占用约 2KB 栈空间,由 Go 运行时动态扩容/缩容。其调度基于 M:N 模型(M 个 OS 线程映射 N 个 goroutine),避免系统线程频繁切换开销。
channel:类型安全的通信管道
// 工业级带缓冲 channel,平衡生产者/消费者速率
jobs := make(chan *Job, 100) // 缓冲区容量为100,防阻塞
results := make(chan *Result, 100)
jobs 缓冲容量设为 100,既避免上游突增压垮下游,又防止内存无限增长;results 同理,保障结果回写不丢包。
Worker Pool:可控并发的核心模式
graph TD
A[Producer] -->|jobs| B[Worker Pool]
B -->|results| C[Consumer]
B --> D[Worker 1]
B --> E[Worker 2]
B --> F[Worker N]
| 组件 | 关键参数 | 作用 |
|---|---|---|
| Worker 数量 | runtime.NumCPU() |
利用硬件并行度,避免过度争抢 |
| Channel 容量 | ≥单次峰值负载 | 平滑流量峰谷,降低 panic 风险 |
数据同步机制
使用 sync.WaitGroup 精确等待所有 worker 完成,并配合 close(results) 通知消费端终止循环——这是无竞态、可预测的终止协议。
2.3 接口抽象与DDD分层设计:订单领域模型的Go式建模
Go语言不支持继承,但通过接口组合与依赖倒置,可自然表达DDD中的“仓储”“领域服务”等抽象边界。
领域层接口定义
// OrderRepository 定义持久化契约,不暴露实现细节
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
}
Save 和 FindByID 是纯领域语义操作;context.Context 支持超时与取消,符合Go工程实践;参数 *Order 为值对象/聚合根,确保仓储只操作领域内核。
分层职责对照表
| 层级 | 职责 | 典型类型 |
|---|---|---|
| domain | 核心业务规则、不变量校验 | Order, PlaceOrder() |
| application | 用例编排、事务边界 | OrderService |
| infrastructure | MySQL/Redis适配器 | mysqlOrderRepo |
数据流示意
graph TD
A[API Handler] --> B[Application Service]
B --> C[Domain Model]
C --> D[OrderRepository]
D --> E[(MySQL)]
2.4 错误处理与可观测性基建:自定义error wrap、context传递与结构化日志
自定义错误包装:语义化与可追溯性
Go 中原生 errors 包支持 fmt.Errorf 和 errors.Unwrap,但缺乏上下文携带能力。推荐使用 github.com/pkg/errors 或 Go 1.13+ 的 errors.Join 与 %w 动词:
func fetchUser(ctx context.Context, id int) (User, error) {
if id <= 0 {
return User{}, fmt.Errorf("invalid user ID %d: %w", id, ErrInvalidParam)
}
// ... HTTP call
if err != nil {
return User{}, fmt.Errorf("failed to fetch user %d from API: %w", id, err)
}
return user, nil
}
%w 触发错误链封装,支持 errors.Is() 和 errors.As() 精准匹配;ctx 不直接嵌入 error,但可通过 context.WithValue 或中间件统一注入 traceID。
结构化日志与上下文透传
使用 log/slog(Go 1.21+)或 zerolog 输出 JSON 日志,自动注入 trace_id、span_id、service_name:
| 字段 | 类型 | 说明 |
|---|---|---|
trace_id |
string | 全局唯一请求标识 |
level |
string | error/warn/info |
err_code |
string | 业务错误码(如 USER_NOT_FOUND) |
可观测性协同流程
graph TD
A[HTTP Handler] --> B[Context with TraceID]
B --> C[Service Layer]
C --> D[Error Wrap + Context Log]
D --> E[Structured Log Sink]
E --> F[ELK / Grafana Loki]
关键实践:所有 error 必须 wrap,所有 log 必须结构化,所有 context 必须透传——三者构成可观测性闭环。
2.5 Go泛型在订单服务中的落地:类型安全的限流策略与熔断状态机
泛型限流器:统一接口,多类型适配
为适配 OrderID(string)、UserID(int64)等不同维度的限流键,定义泛型限流器:
type Limiter[T comparable] interface {
Allow(key T) bool
Reserve(key T) *Reservation[T]
}
type TokenBucketLimiter[T comparable] struct {
buckets sync.Map // map[T]*tokenBucket
rate float64
cap int64
}
T comparable约束确保键可哈希;sync.Map避免全局锁,提升高并发下OrderID和UserID的独立限流性能;rate与cap按业务维度独立配置。
类型安全的熔断状态机
| 状态 | 触发条件 | 类型约束体现 |
|---|---|---|
| Closed | 连续成功调用 ≥ threshold | T 决定统计键粒度(如 OrderID) |
| Open | 错误率 > 50% 且持续 30s | 状态迁移函数泛型化:func (c *CircuitBreaker[T]) Transition() |
| HalfOpen | Open 超时后首次探测 | 探测请求携带 T 实例,保障上下文一致性 |
熔断决策流程
graph TD
A[Incoming Order Request] --> B{Is key T valid?}
B -->|Yes| C[Check Circuit State for T]
C --> D[Closed → Call Service]
C --> E[Open → Return ErrCircuitOpen]
D --> F{Success?}
F -->|Yes| G[Update success counter for T]
F -->|No| H[Update failure counter for T]
第三章:弹性架构三支柱——熔断/限流/链路追踪内核实现
3.1 基于Hystrix思想的轻量熔断器:状态跃迁、半开探测与指标采样
熔断器核心在于三态协同:关闭(Closed)→ 打开(Open)→ 半开(Half-Open),状态跃迁由实时指标驱动。
状态机跃迁逻辑
// 熔断器状态跃迁核心判定(简化版)
if (state == CLOSED && failureRate > threshold) {
state = OPEN;
openTimestamp = System.currentTimeMillis();
} else if (state == OPEN && elapsed() > timeout) {
state = HALF_OPEN; // 自动触发探测
}
逻辑分析:
failureRate基于滑动窗口内失败请求数占比计算;timeout为固定恢复期(如60s),确保服务有冷却时间;elapsed()避免时钟回拨风险,采用单调递增时间源。
指标采样策略对比
| 采样方式 | 精度 | 内存开销 | 适用场景 |
|---|---|---|---|
| 固定窗口计数 | 低 | 极小 | QPS |
| 滑动时间窗 | 中高 | 中等 | 主流HTTP API |
| 令牌桶动态采样 | 高 | 较高 | 高频异步调用链 |
半开探测机制
- 仅允许单个请求通过(
probeRequest) - 成功则重置统计并切回
CLOSED - 失败则重置
OPEN并延长超时周期(指数退避)
graph TD
A[Closed] -->|错误率超阈值| B[Open]
B -->|超时到期| C[Half-Open]
C -->|探测成功| A
C -->|探测失败| B
3.2 多算法限流器选型与集成:令牌桶/漏桶/滑动窗口在下单峰值场景对比验证
下单峰值典型特征
- 突发性:秒杀开始瞬间 QPS 激增 5–10 倍
- 短时性:持续时间通常 ≤ 30 秒
- 可容忍丢弃:非核心用户请求可降级拒绝
核心算法行为对比
| 算法 | 平滑性 | 突发承载 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 令牌桶 | ✅ | ✅✅✅ | 中 | 高突发+需允许瞬时爆发 |
| 漏桶 | ✅✅✅ | ❌ | 低 | 强匀速控制(如短信) |
| 滑动窗口 | ❌ | ✅✅ | 高 | 精确统计+短周期限流 |
令牌桶限流关键实现(Spring Cloud Gateway)
// 基于 Redis + Lua 的分布式令牌桶(原子性保障)
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 令牌生成速率(token/s)
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])
local lastTime = redis.call('HGET', key, 'last_time')
local tokens = tonumber(redis.call('HGET', key, 'tokens') or capacity)
if lastTime then
local delta = math.min(rate * (now - lastTime), capacity)
tokens = math.min(capacity, tokens + delta)
end
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'last_time', now)
return 1 -- 允许通过
else
return 0 -- 拒绝
end
该脚本确保每秒按 rate 补充令牌,最大不超过 capacity;last_time 记录上次更新时间,避免时钟漂移导致令牌误补;Lua 原子执行杜绝并发竞争。
流量调度决策流程
graph TD
A[请求到达] --> B{是否在滑动窗口内?}
B -->|否| C[直接拒绝]
B -->|是| D[尝试获取令牌桶token]
D -->|成功| E[转发至订单服务]
D -->|失败| F[降级为漏桶匀速放行或返回429]
3.3 OpenTelemetry标准链路追踪:Span注入、跨进程上下文传播与Jaeger后端对接
OpenTelemetry 提供统一的可观测性接入标准,其核心在于 Span 生命周期管理 与 上下文透传机制。
Span 创建与注入示例
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
# 初始化 tracer(自动注入全局上下文)
trace.set_tracer_provider(TracerProvider())
jaeger_exporter = JaegerExporter(agent_host_name="localhost", agent_port=6831)
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(jaeger_exporter))
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("http.request") as span:
span.set_attribute("http.method", "GET")
span.set_attribute("http.url", "/api/users")
此代码创建根 Span 并自动绑定至当前线程上下文;
BatchSpanProcessor异步推送 Span 数据至 Jaeger Agent;set_attribute为 Span 添加语义化标签,供后端查询与过滤。
跨进程传播关键载体
OpenTelemetry 默认使用 W3C TraceContext(traceparent/tracestate)实现 HTTP 跨服务传递,兼容主流框架中间件(如 Flask、FastAPI 的自动注入)。
Jaeger 兼容性要点
| 组件 | 要求 |
|---|---|
| 协议 | Thrift over UDP/HTTP |
| 端点格式 | agent_host_name:agent_port |
| 采样策略 | 由 Jaeger Agent 统一控制 |
graph TD
A[Service A] -->|HTTP + traceparent| B[Service B]
B -->|gRPC + baggage| C[Service C]
A & B & C --> D[Jaeger Agent]
D --> E[Jaeger Collector]
E --> F[Jaeger UI]
第四章:订单服务全链路工程闭环
4.1 REST/gRPC双协议API设计:订单创建、查询、状态机变更的契约驱动开发
统一契约定义(OpenAPI + Protobuf)
使用 order.proto 同时生成 gRPC 接口与 OpenAPI 3.0 文档,确保语义一致性:
// order.proto
message CreateOrderRequest {
string user_id = 1 [(validate.rules).string.min_len = 1];
repeated OrderItem items = 2 [(validate.rules).repeated.min_items = 1];
}
逻辑分析:
user_id强制非空校验,items至少含一项;gRPC Server 自动注入google.api.expr验证逻辑,REST 网关通过grpc-gateway映射为/v1/ordersPOST 接口,字段级约束同步生效。
协议适配层行为对比
| 功能 | REST(JSON) | gRPC(Protobuf) |
|---|---|---|
| 序列化效率 | 中等(文本解析开销) | 高(二进制+Schema绑定) |
| 错误码语义 | HTTP 状态码 + error 字段 | gRPC status code + details |
状态机变更流程
graph TD
A[CREATE_REQUEST] -->|valid| B[PAID]
B -->|timeout| C[CANCELLED]
B -->|ship| D[SHIPPED]
D -->|deliver| E[COMPLETED]
状态跃迁由 UpdateOrderStatusRequest 的 expected_state 字段强制校验,避免非法跳转。
4.2 数据持久化与一致性保障:MySQL事务嵌套、Redis缓存穿透防护与分布式ID生成
MySQL事务嵌套的实践边界
MySQL原生不支持真正的事务嵌套,SAVEPOINT 是唯一可控的“伪嵌套”机制:
START TRANSACTION;
INSERT INTO orders VALUES (1, 'A');
SAVEPOINT sp1;
INSERT INTO items VALUES (101, 1, 'book'); -- 若失败,可回滚至此
ROLLBACK TO SAVEPOINT sp1; -- 不影响orders插入
COMMIT;
SAVEPOINT sp1创建轻量级回滚点;ROLLBACK TO仅撤销其后语句,不影响前置操作;注意事务隔离级别(如REPEATABLE READ)对可见性的影响。
Redis缓存穿透防护策略
- 布隆过滤器预检(拦截99%无效key)
- 空值缓存(
SET key "" EX 60,防雪崩) - 请求合并(Redisson
RBatch批量兜底)
分布式ID方案对比
| 方案 | QPS上限 | 单调递增 | 时钟依赖 |
|---|---|---|---|
| Snowflake | 10w+ | ✅ | ✅ |
| Redis INCR | 5w | ✅ | ❌ |
| UUIDv4 | ∞ | ❌ | ❌ |
graph TD
A[请求ID] --> B{是否已存在?}
B -->|是| C[返回缓存ID]
B -->|否| D[调用Snowflake服务]
D --> E[写入Redis缓存 5m]
E --> C
4.3 单元测试/集成测试/混沌测试三位一体:使用testify+gomock+chaos-mesh验证弹性边界
现代云原生系统需在代码层、服务层与基础设施层同步验证韧性。单元测试聚焦逻辑正确性,集成测试校验组件协同,混沌测试则主动注入故障以暴露系统盲区。
测试分层职责对比
| 层级 | 工具栈 | 验证目标 | 典型场景 |
|---|---|---|---|
| 单元测试 | testify + gomock | 接口契约与分支覆盖 | 依赖隔离下的错误处理 |
| 积成测试 | testify + Docker | API 响应一致性与时序 | 多服务调用链路连通性 |
| 混沌测试 | Chaos Mesh | 故障传播抑制与自愈能力 | Pod 网络延迟/磁盘满压测 |
模拟依赖的gomock示例
// 构建MockClient用于隔离外部HTTP依赖
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockSvc := mocks.NewMockExternalService(mockCtrl)
mockSvc.EXPECT().
FetchUser(gomock.Any(), "u123").
Return(&User{Name: "Alice"}, nil).
Times(1) // 显式约束调用次数
该段代码通过gomock生成接口桩,Times(1)确保被测函数仅调用一次外部服务,避免测试偶然性;gomock.Any()放宽上下文参数匹配,聚焦业务逻辑验证。
弹性验证闭环流程
graph TD
A[单元测试:断言panic恢复] --> B[集成测试:验证重试+熔断]
B --> C[Chaos Mesh注入网络分区]
C --> D[观测指标:P99延迟≤2s & 错误率<0.5%]
4.4 CI/CD流水线与SRE就绪:GitHub Actions自动化构建、Prometheus指标暴露与Grafana看板配置
自动化构建:GitHub Actions工作流核心逻辑
# .github/workflows/ci-cd.yml
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Build & Test
run: |
go build -o app ./cmd/
go test -v -race ./...
- name: Push to Registry
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
该工作流实现语义化版本触发(refs/tags/)的镜像构建与推送,-race启用竞态检测保障SRE可观测性基线。
指标暴露:Prometheus端点集成
应用需暴露 /metrics 端点,返回标准文本格式指标(如 http_requests_total{method="GET",status="200"} 123),由Prometheus定时抓取。
可视化闭环:Grafana看板关键维度
| 面板类别 | 核心指标 | SLO关联性 |
|---|---|---|
| 构建健康度 | github_actions_workflow_run_total |
构建成功率 ≥99.5% |
| 服务可用性 | http_up{job="app"} |
Uptime ≥99.9% |
| 延迟P95 | http_request_duration_seconds |
graph TD
A[GitHub Push] --> B[Actions构建+推送镜像]
B --> C[Pod启动并暴露/metrics]
C --> D[Prometheus定时抓取]
D --> E[Grafana实时渲染看板]
E --> F[SRE告警与容量决策]
第五章:验收Checklist与高阶演进路径
核心交付物验收清单
以下为某金融级微服务系统上线前的强制性验收项(✓ 表示通过,✗ 表示阻断):
| 检查项 | 验证方式 | 通过标准 | 实际结果 |
|---|---|---|---|
| 全链路灰度发布能力 | 在K8s集群中部署v2.1版本至5%流量节点 | 请求路由准确率≥99.99%,无跨版本数据污染 | ✓ |
| 敏感字段动态脱敏 | 向用户中心API发起含身份证号的GET请求 | 响应体中id_card字段始终返回***掩码,且审计日志记录脱敏动作 |
✓ |
| 熔断器状态持久化 | 手动触发下游支付服务超时,持续30秒后恢复 | Hystrix Dashboard显示熔断状态在Pod重启后仍保持OPEN,10秒内自动半开 | ✓ |
| Prometheus指标完整性 | 查询http_request_duration_seconds_count{service="order"}[1h] |
至少包含status_code, endpoint, region三个label维度,采样间隔≤15s |
✗(缺失region标签,需重配Relabel规则) |
生产环境故障注入验证
采用Chaos Mesh对订单服务执行真实扰动:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkDelay
metadata:
name: order-db-latency
spec:
mode: one
selector:
namespaces:
- production
labelSelectors:
app: order-service
target:
selector:
labelSelectors:
app: mysql-primary
mode: one
latency: "100ms"
correlation: "0.3"
验证期间监控发现:订单创建TP99从120ms升至480ms,但成功率维持99.2%(阈值≥98.5%),证明降级策略生效;同时发现库存服务未同步启用熔断,暴露依赖治理盲区。
多云架构演进路线图
某跨境电商平台在完成单AZ Kubernetes集群后,启动分阶段多云迁移:
- 阶段一(Q3 2024):在阿里云华东1与AWS us-west-2部署双活Ingress,通过Global Traffic Manager实现DNS权重调度,实测跨云切换RTO
- 阶段二(Q1 2025):将核心商品目录服务容器化改造为eBPF加速的Service Mesh Sidecar,消除Envoy代理CPU开销37%;
- 阶段三(Q4 2025):基于OpenTelemetry Collector构建统一遥测管道,接入Azure Monitor与Datadog双后端,满足GDPR数据主权要求——所有欧盟用户trace数据仅落库于Azure德国法兰克福Region。
安全合规硬性红线
在PCI DSS Level 1认证过程中,必须满足以下不可协商条款:
- 所有数据库连接字符串禁止出现在任何配置文件中,须通过HashiCorp Vault动态注入;
- 每次CI/CD流水线构建镜像后,自动触发Trivy扫描,CVE-CVSS≥7.0的漏洞导致Pipeline立即终止;
- API网关层强制TLS 1.3+,且禁用所有弱密码套件(如
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA); - 审计日志保留周期≥365天,并通过WORM(Write Once Read Many)存储介质固化。
成本优化关键杠杆
某视频平台通过精细化资源画像实现32%云支出下降:
- 使用Kubecost采集各Namespace CPU/内存Request/Usage比值,识别出
recommendation-engine命名空间平均Request利用率仅21%; - 结合VictoriaMetrics历史负载曲线,将该服务HPA策略从
cpuUtilization: 70%调整为customMetric: video_encode_queue_length > 500; - 对GPU节点池启用NVIDIA MIG切分,单张A100显卡运行3个隔离实例,推理吞吐提升2.1倍。
Mermaid流程图展示自动化验收流水线编排逻辑:
flowchart TD
A[Git Tag v2.3.0] --> B[触发CI Pipeline]
B --> C{静态扫描}
C -->|通过| D[构建镜像并Push至Harbor]
C -->|失败| E[阻断并通知SLACK#devops]
D --> F[部署至Staging集群]
F --> G[运行Selenium端到端测试]
G -->|全部通过| H[生成验收报告PDF]
G -->|失败| I[标记Failed并归档Jenkins Build Log]
H --> J[人工复核Checklist签字] 