第一章:千亿级装备制造商的技术债困局与Go迁移战略决策
某全球Top 3重型装备制造商,其核心MES(制造执行系统)与PLM(产品生命周期管理)平台已运行逾12年,底层由Java 6+ Spring 2.x、Perl脚本、Oracle Forms及大量Shell胶水代码构成。系统日均处理超8.7亿条设备工况数据、支撑23个生产基地的实时排程与质量追溯,但技术栈碎片化严重:47个独立部署单元中,32个使用不同版本的Spring框架,19个仍依赖已终止维护的WebLogic 10.3;平均每次关键补丁发布需跨7个团队协调,平均修复一个P1级生产阻断缺陷耗时58小时。
技术债集中体现为三重断裂带:
- 协议断裂:新IoT边缘网关采用gRPC/HTTP2,而遗留服务仅支持SOAP over HTTP/1.1且无TLS 1.2支持;
- 运维断裂:Kubernetes集群无法纳管Java应用的JVM堆外内存泄漏进程,监控盲区达34%;
- 人才断裂:熟悉COBOL+DB2批处理模块的工程师平均年龄59岁,近三年无新人能独立维护该子系统。
面对“既要保障产线零停机,又要三年内完成云原生重构”的刚性目标,架构委员会启动多语言评估矩阵:
| 维度 | Go | Rust | Java 17 | Node.js |
|---|---|---|---|---|
| 内存安全 | ✅ GC可控 | ✅ 零成本 | ⚠️ JNI风险 | ❌ GC不可控 |
| 跨平台交叉编译 | ✅ 原生支持 | ✅ 支持 | ⚠️ JVM绑定 | ❌ 依赖Node ABI |
| 现有团队学习曲线 | ⚠️ 中等(需重学并发模型) | ❌ 高陡峭 | ✅ 低(语法兼容) | ✅ 低 |
最终选择Go的核心动因在于其静态链接特性可彻底消除Linux发行版差异导致的“在我机器上能跑”问题。迁移首阶段聚焦于构建统一通信中间件——用Go重写所有服务间调用代理层:
// service-proxy/main.go:轻量级gRPC-to-REST反向代理,兼容旧Java服务
func main() {
// 启动gRPC服务端,监听443端口(复用现有HTTPS证书)
lis, _ := net.Listen("tcp", ":443")
s := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{loadCert()}, // 复用原有PKI体系
})))
// 注册适配器:将gRPC请求自动转为HTTP/1.1 POST至legacy-java-service:8080
pb.RegisterProxyServiceServer(s, &proxyAdapter{})
s.Serve(lis)
}
该代理层上线后,旧Java服务无需任何代码修改,即可被新Go微服务直接调用,为后续渐进式替换奠定协议基座。
第二章:遗留C++系统解构与Go现代化重构方法论
2.1 C++工业控制模块的语义映射与Go类型建模实践
工业控制协议(如IEC 61131-3)中BOOL、INT、TIME等类型需在Go中保持语义一致性与内存布局可控性。
类型安全映射策略
BOOL→bool(零值语义对齐,避免uint8歧义)INT→int16(显式宽度,匹配PLC 16位寄存器)TIME→ 自定义type TIME time.Duration(纳秒精度,支持T#5S解析)
Go结构体建模示例
type MotorStatus struct {
Running bool `json:"running"` // 对应C++ bool m_bRunning
Speed int16 `json:"speed"` // 对应C++ int16_t m_nSpeed
Timeout TIME `json:"timeout"` // 对应C++ TIME m_tTimeout
}
该结构体满足
unsafe.Sizeof()与C++ ABI兼容(无填充膨胀),json标签支持HMI层序列化;TIME类型封装了ParseIECTime()方法,可解析T#100MS格式字符串。
映射验证表
| C++类型 | Go类型 | 内存占用 | 语义约束 |
|---|---|---|---|
BOOL |
bool |
1 byte | 非0即true |
DINT |
int32 |
4 bytes | 补码,大端兼容 |
graph TD
A[C++ Struct] -->|ABI对齐| B[Go struct]
B --> C[JSON API]
B --> D[Shared Memory]
2.2 实时性保障机制迁移:从C++ RTOS调度到Go goroutine+chan协同模型
数据同步机制
RTOS中依赖优先级抢占与信号量保护临界区;Go则通过chan天然实现同步,避免显式锁。
// 实时传感器采样协程(周期10ms)
func sensorReader(ch chan<- int, ticker *time.Ticker) {
for range ticker.C {
select {
case ch <- readADC(): // 非阻塞写入,背压自动限流
default: // 丢弃过期样本,保障时效性
}
}
}
ch为带缓冲通道(如make(chan int, 2)),容量=最大允许延迟采样数;default分支实现硬实时丢弃策略,等效于RTOS的osMessageQueuePut(..., osWaitNo)。
调度语义对比
| 维度 | C++ RTOS(如FreeRTOS) | Go runtime |
|---|---|---|
| 调度单位 | 任务(Task) | goroutine |
| 切换开销 | ~1.2μs(上下文保存) | ~20ns(用户态协程) |
| 优先级控制 | 静态抢占式 | 无显式优先级,靠channel调度隐式约束 |
graph TD
A[传感器中断] --> B{Go runtime}
B --> C[唤醒sensorReader goroutine]
C --> D[写入带缓冲channel]
D --> E[main goroutine select接收]
E --> F[实时处理逻辑]
2.3 硬件抽象层(HAL)的Go接口化重构与跨平台驱动适配
传统 HAL 多以 C 接口耦合平台,Go 生态中需解耦硬件细节与业务逻辑。核心思路是定义行为契约而非实现。
接口契约设计
type Device interface {
Open(path string) error
Read(buf []byte) (int, error)
Write(buf []byte) (int, error)
Close() error
}
Open 接收设备路径(如 /dev/i2c-1 或 Windows COM3),Read/Write 统一处理字节流,屏蔽 ioctl 或 DeviceIoControl 差异;Close 保障资源释放。
跨平台驱动适配策略
| 平台 | 实现包 | 关键适配点 |
|---|---|---|
| Linux | github.com/godriver/i2c |
使用 sysfs + ioctl 封装 |
| Windows | golang.org/x/sys/windows |
基于 CreateFile + DeviceIoControl |
| ESP32 (TinyGo) | machine |
直接映射寄存器操作 |
初始化流程
graph TD
A[NewHAL] --> B{OS == “windows”?}
B -->|Yes| C[WinDriver]
B -->|No| D[LinuxDriver]
C --> E[Device Interface]
D --> E
重构后,上层服务仅依赖 Device 接口,驱动替换无需修改业务代码。
2.4 原有CORBA/DCOM通信栈向gRPC+Protobuf v3工业协议栈的渐进式替换
演进动因
遗留系统中CORBA IDL编译器耦合强、DCOM跨平台支持差,而gRPC+Protobuf v3提供语言中立、零拷贝序列化与HTTP/2流控能力,契合工业场景对低延迟(
协议映射对照
| CORBA概念 | DCOM对应 | gRPC+Protobuf v3等价实现 |
|---|---|---|
| Interface定义 | COM Interface | .proto service + rpc method |
| Any类型传输 | VARIANT | google.protobuf.Any |
| 异步调用 | Async moniker | ServerStreaming / Bidirectional streaming |
渐进式迁移路径
// industrial_sensor.proto(v3)
syntax = "proto3";
package industrial.v1;
message SensorReading {
int64 timestamp_ms = 1;
float temperature_c = 2;
bool is_valid = 3;
}
service SensorService {
rpc StreamReadings(stream SensorReading) returns (stream SensorReading); // 支持双向流,替代CORBA push model
}
此定义启用
StreamReadings双向流,复用现有传感器心跳周期,无需修改设备固件;timestamp_ms采用int64而非google.protobuf.Timestamp,规避嵌套解析开销,实测降低端侧CPU占用12%。
graph TD A[Legacy CORBA Client] –>|IDL stub| B[Adapter Gateway] C[DCOM Server] –>|COM wrapper| B B –>|gRPC unary/stream| D[New SensorService] D –> E[(Time-Series DB)]
2.5 静态链接库依赖图谱分析与Go Module依赖治理双轨策略
静态链接库(如 .a 文件)的隐式依赖常导致构建不一致与安全盲区;而 Go Module 的 go.mod 显式声明虽清晰,却难以反映底层 C/C++ 依赖的真实拓扑。
依赖图谱可视化
graph TD
A[main.go] --> B[libnet.a]
B --> C[openssl.a]
B --> D[zlib.a]
C --> E[libc.a]
Go Module 治理关键实践
- 使用
go mod graph | grep提取模块间引用关系 - 通过
go list -f '{{.Deps}}' ./...批量导出依赖树 - 在
go.mod中用replace显式约束 Cgo 依赖版本锚点
安全协同治理表
| 维度 | 静态库依赖图谱 | Go Module 依赖树 |
|---|---|---|
| 可见性 | 需 nm -C libnet.a 解析符号 |
go mod graph 直接输出 |
| 版本溯源 | 无元数据,依赖构建环境 | go.sum 提供校验哈希 |
| 漏洞修复路径 | 需重新编译整个 .a |
go get example.com/lib@v1.2.3 即时更新 |
双轨并行:图谱扫描工具(如 ldd -v + objdump 脚本)与 go mod verify 形成互补验证闭环。
第三章:高可靠制造场景下的Go工程落地体系构建
3.1 ISO 13849-1安全等级要求与Go内存安全边界验证框架
ISO 13849-1定义了PL(Performance Level)从a到e的五级安全要求,其中PLd要求单通道失效检测覆盖率≥90%,且必须排除未定义行为(UB)——这与Go运行时禁止指针算术、强制边界检查的内存安全模型天然契合。
验证框架核心约束
- 强制启用
-gcflags="-d=checkptr"捕获非法指针转换 - 禁用
unsafe.Pointer隐式类型穿透路径 - 所有切片访问经
runtime.checkSlice动态校验
内存边界校验代码示例
// 安全切片访问封装(符合PLd数据完整性要求)
func SafeReadAt[T any](s []T, i int) (T, bool) {
if i < 0 || i >= len(s) {
var zero T
return zero, false // 显式失败路径,避免panic扰动安全状态
}
return s[i], true
}
该函数在编译期绑定GOEXPERIMENT=safeslice,确保索引检查不可绕过;返回布尔值替代panic,满足ISO 13849-1对“可预测故障响应”的PLd级要求。
| PL等级 | 要求故障检测率 | Go对应机制 |
|---|---|---|
| PLc | ≥60% | go build -race |
| PLd | ≥90% | checkptr + safeslice |
| PLe | ≥99% | CGO禁用 + 内存隔离沙箱 |
graph TD
A[源码] --> B[go build -gcflags=-d=checkptr]
B --> C{运行时指针合法性检查}
C -->|合法| D[执行安全内存访问]
C -->|非法| E[立即中止并记录安全事件]
3.2 实时数据采集微服务集群的Go并发模型调优与确定性延迟控制
为保障毫秒级端到端延迟(P99 ≤ 12ms),集群采用分层并发模型:采集协程池 + 确定性缓冲队列 + 固定周期调度器。
数据同步机制
使用带超时控制的 sync.Pool 复用 *bytes.Buffer,避免 GC 波动引入抖动:
var bufPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 1024)) // 预分配1KB,规避扩容抖动
},
}
New函数返回预分配容量的 buffer,实测降低 37% 内存分配延迟;1024基于典型日志事件中位长度设定,兼顾空间效率与缓存局部性。
调度策略对比
| 策略 | 平均延迟 | P99延迟 | CPU波动 |
|---|---|---|---|
| runtime.Gosched() | 8.2ms | 15.6ms | 高 |
| time.Sleep(100μs) | 7.9ms | 11.3ms | 中 |
| channel select with timeout | 7.1ms | 10.2ms | 低 |
流控拓扑
graph TD
A[采集协程] -->|背压信号| B[Token Bucket]
B --> C{令牌充足?}
C -->|是| D[写入RingBuffer]
C -->|否| E[Drop & Metric+]
3.3 制造执行系统(MES)核心事务链路的Go分布式Saga模式实现
在多车间、多设备协同的MES场景中,订单排程、工单下发、工艺参数写入、设备状态校验等操作跨服务边界,需强一致性保障。传统两阶段提交(2PC)因阻塞与单点故障被弃用,Go语言生态下采用Choreographed Saga实现最终一致性。
核心事务链路分解
- 工单创建 → 工艺绑定 → 设备预约 → 参数下发 → 状态确认
- 每步含正向操作(
Do)与补偿操作(Undo),失败时逆序触发补偿
Saga协调器关键结构
type SagaStep struct {
Name string
Do func(ctx context.Context, data map[string]interface{}) error
Undo func(ctx context.Context, data map[string]interface{}) error
Timeout time.Duration // 各步骤超时控制,防长事务阻塞
}
Timeout确保设备通信异常时快速降级;data为全局上下文透传载体,含工单ID、工序版本号等幂等标识。
状态迁移流程
graph TD
A[Start] --> B[CreateWorkOrder]
B --> C[BindProcess]
C --> D[ReserveEquipment]
D --> E[PushParameters]
E --> F[ConfirmStatus]
F --> G[Success]
B -.-> H[UndoCreate]
C -.-> I[UndoBind]
D -.-> J[UndoReserve]
E -.-> K[UndoPush]
F -.-> L[UndoConfirm]
| 步骤 | 幂等键字段 | 补偿触发条件 |
|---|---|---|
| 工单创建 | order_id |
DB插入失败或下游回调超时 |
| 参数下发 | device_id+version |
设备返回NACK或心跳超时 |
第四章:17万行代码迁移全周期实战路径图
4.1 分阶段切流策略:从边缘PLC网关服务到主控逻辑模块的灰度迁移路线
灰度迁移以“流量可度量、状态可回滚、依赖可解耦”为设计前提,分三阶段推进:
- 阶段一(10%):仅透传非关键控制指令,PLC网关保留全部执行权
- 阶段二(50%):主控模块接管逻辑判断,网关降级为协议适配器
- 阶段三(100%):网关切换为只读旁路模式,主控全权调度
数据同步机制
采用双写+版本号校验保障一致性:
def sync_to_main_control(payload):
# payload: {"plc_id": "GW-07", "ts": 1718234567890, "vsn": 3, "cmd": "START"}
if payload["vsn"] > get_local_version(payload["plc_id"]): # 防止旧版本覆盖
push_to_kafka("main-control-in", payload) # 主控消费队列
update_local_version(payload["plc_id"], payload["vsn"])
逻辑分析:
vsn(语义版本号)由网关在每次逻辑升级时递增,主控模块通过比对本地缓存版本决定是否接纳新指令,避免状态错乱。
切流决策表
| 流量比例 | 网关角色 | 主控职责 | 回滚触发条件 |
|---|---|---|---|
| 10% | 全功能执行 | 仅审计日志 | 连续3次指令超时 |
| 50% | 协议转换+心跳 | 执行核心逻辑+异常熔断 | 5分钟内错误率 > 0.5% |
| 100% | 只读上报 | 全链路闭环控制 | 人工干预开关 |
graph TD
A[边缘PLC网关] -->|阶段一:10%指令| B(主控逻辑模块)
A -->|90%直连PLC| C[本地执行引擎]
B -->|反馈校验结果| A
4.2 C++/Go混合编译环境搭建与cgo性能陷阱规避指南
混合构建基础:CMake + CGO 协同
需在 CGO_CPPFLAGS 中显式传递 C++ 头路径与标准库选项:
export CGO_CPPFLAGS="-I./cpp/include -std=c++17"
export CGO_LDFLAGS="-L./cpp/lib -lmycore -lc++"
逻辑说明:
CGO_CPPFLAGS影响 Go 编译器调用的 C++ 预处理器行为,必须包含-std=c++17以匹配 C++ 源码特性;CGO_LDFLAGS控制链接阶段,-lc++显式链接 LLVM libc++(而非默认 libstdc++),避免 ABI 冲突。
常见 cgo 性能陷阱对照表
| 陷阱类型 | 表现 | 推荐方案 |
|---|---|---|
| 频繁跨语言调用 | 每次调用触发 Goroutine 栈切换 | 批量接口设计(如 ProcessBatch) |
| C 字符串拷贝 | C.CString() 产生堆分配 |
复用 unsafe.Slice + C.GoBytes |
内存生命周期关键流程
graph TD
A[Go 字符串] -->|C.CString| B[C heap 分配]
B --> C[cgo 函数调用]
C --> D[手动 C.free]
D --> E[避免悬垂指针]
4.3 基于eBPF的运行时行为观测体系:覆盖CAN总线报文、伺服周期抖动、IO响应延迟
传统内核模块难以安全、动态地捕获实时工业总线行为。eBPF 提供零侵入、可验证、热加载的观测能力,成为构建高保真运行时监控体系的核心载体。
多源事件统一采集架构
// can_rx_trace.c —— 挂载在 CAN RX softirq 上下文
SEC("tracepoint/net/netif_receive_skb")
int trace_can_frame(struct trace_event_raw_netif_receive_skb *ctx) {
struct can_frame *cf = (struct can_frame *)ctx->skb->data;
if (is_can_bus_skb(ctx->skb)) { // 通过 skb->dev->name 匹配 can0/can1
bpf_perf_event_output(ctx, &can_events, BPF_F_CURRENT_CPU, cf, sizeof(*cf));
}
return 0;
}
该程序利用 tracepoint 精确捕获原始 CAN 帧,避免 socket 过滤开销;is_can_bus_skb() 通过设备名白名单保障匹配准确性;bpf_perf_event_output 实现零拷贝用户态流式消费。
关键指标联动分析维度
| 指标类型 | 触发点 | 采样精度 | 典型抖动容忍 |
|---|---|---|---|
| CAN 报文间隔 | can_rx tracepoint |
±5μs | |
| 伺服周期(PVT) | hrtimer_start kprobe |
~100ns | ±2μs |
| GPIO 中断响应 | irq_handler_entry |
~300ns | ±8μs |
graph TD
A[内核事件源] --> B{eBPF 程序}
B --> C[环形缓冲区]
C --> D[用户态聚合器]
D --> E[时序对齐引擎]
E --> F[联合抖动热力图]
4.4 迁移质量门禁:静态分析(go vet + custom SSA pass)、FMEA用例回归、硬件在环(HIL)测试覆盖率达标方案
静态分析双引擎协同
在 CI 流水线中集成 go vet 基础检查与自定义 SSA 分析器,后者识别未初始化的通道写入模式:
// custom-ssa-pass/main.go
func run(pass *analysis.Pass) (interface{}, error) {
for _, fn := range pass.Funcs {
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
if store, ok := instr.(*ssa.Store); ok {
if isUninitializedChan(store.Addr, pass) {
pass.Reportf(store.Pos(), "unsafe channel write: uninitialized receiver") // 触发门禁拦截
}
}
}
}
}
return nil, nil
}
该 pass 基于 golang.org/x/tools/go/analysis 框架构建,isUninitializedChan 利用数据流敏感的指针分析判定通道变量是否在写入前完成 make(chan) 初始化;pass.Reportf 触发非零退出码,阻断 PR 合并。
FMEA 回归验证矩阵
| 失效模式 | 覆盖用例数 | HIL 实测通过率 |
|---|---|---|
| CAN ID 冲突 | 12 | 100% |
| 电源跌落 >200ms | 8 | 93.7% → 100%* |
| 温度超限触发复位 | 5 | 100% |
*通过新增 3 个边界时序激励用例达成 100%
HIL 覆盖率提升路径
graph TD
A[原始覆盖率 78%] --> B[注入故障注入控制器]
B --> C[自动生成边缘工况激励序列]
C --> D[实时采集 ECU 响应并比对预期状态机跳转]
D --> E[覆盖率提升至 96.2%]
第五章:制造业Go技术栈的演进边界与产业级反思
车身焊装产线实时质量看板的Go重构实践
某德系合资车企在2022年将原有基于Java Spring Boot + WebSocket的焊点质量监控系统,迁移至Go 1.20 + Gin + GORM + Prometheus生态。核心动因是原系统在单节点承载380+机器人IO点位并发上报时,GC停顿峰值达420ms,导致缺陷告警延迟超1.8秒。重构后采用sync.Pool复用JSON解码缓冲区、goroutine pool限流处理MQTT消息(每秒12,500条焊枪电流波形采样包),P99延迟压降至67ms。关键代码片段如下:
// 焊点数据预处理池化管理
var pointPool = sync.Pool{
New: func() interface{} {
return &WeldPoint{Timestamp: time.Now(), Values: make([]float64, 0, 256)}
},
}
工业协议网关的内存安全边界挑战
在对接西门子S7-1200 PLC时,团队发现使用gopcua库直接解析二进制响应存在内存越界风险——当PLC返回异常长度的字符串字段(如固件版本含不可见控制字符),Go的unsafe.Slice操作触发SIGSEGV。最终通过引入github.com/tidwall/gjson替代原生encoding/json,并增加协议层校验:对S7报文头中DataLength字段执行if dataLen > 65535 { return ErrInvalidPacket }硬约束,使网关月均崩溃次数从3.2次降至0。
边缘计算集群资源调度失衡现象
下表对比了三种调度策略在12台ARM64边缘节点(每台4GB RAM)上的实测表现:
| 调度策略 | 平均CPU利用率 | 内存溢出事件/月 | 任务冷启动延迟 |
|---|---|---|---|
| Kubernetes原生调度 | 58% | 7 | 3.2s |
| Go自研轻量调度器 | 82% | 0 | 147ms |
| 基于eBPF的QoS感知调度 | 76% | 0 | 210ms |
自研调度器采用time.Ticker驱动的周期性资源探针(每500ms采集cgroup v2指标),结合设备数字孪生体的实时负载预测模型动态调整Pod亲和性标签。
微服务治理的“非功能性债务”累积
某MES微服务群在接入Go-kit后暴露出典型反模式:17个服务共用同一套transport/http中间件链,其中包含未做熔断的旧版Redis连接池(github.com/go-redis/redis/v7)。2023年Q3因Redis主从切换导致3个核心服务雪崩,根本原因在于redis.NewClient()未配置MaxRetries: 2且缺乏context.WithTimeout包裹。后续强制推行中间件契约规范:所有网络调用必须声明timeout: 500ms与retry: 3元数据,并通过go vet插件静态检查。
跨厂商设备接入的语义鸿沟
在整合发那科CNC与国产华中数控设备时,发现同一G代码指令(如G01 X100.0 F500)在两套设备的OPC UA信息模型中映射路径截然不同:
- 发那科:
ns=2;s=Axis.X.TargetPosition - 华中数控:
ns=3;s=Machine.Axis[0].CommandPos
团队开发了Go编写的规则引擎goplc-rule,支持YAML定义字段映射规则(含单位换算、坐标系转换等),使新设备接入周期从平均14人日压缩至2.5人日。
开源组件选型的隐性成本陷阱
对比ent与sqlc在设备台账服务中的落地效果:ent生成的代码体积达2.1MB(含冗余GraphQL Schema),导致Docker镜像构建时间增加47%;而sqlc生成的纯SQL绑定代码仅142KB,且通过sqlc generate --schema queries.sql --queries queries.sql实现零运行时反射。但sqlc不支持复杂JOIN查询的结构体嵌套,需额外编写DeviceWithStatus手动组装逻辑。
产线变更引发的API契约断裂
当某产线升级ABB机器人固件后,其REST API返回的/api/v1/joints响应中temperature字段由整数变为浮点数。Go客户端使用json.Number解析失败,引发strconv.ParseInt: parsing "23.5": invalid syntax错误。解决方案是改用json.RawMessage延迟解析,并建立字段类型变更检测机制:每日扫描OpenAPI 3.0文档,比对components.schemas.*.properties.*.type历史快照,自动触发回归测试。
安全合规性倒逼架构重构
依据《工业控制系统信息安全防护指南》第12条,所有设备访问必须实现双向mTLS。团队将原有HTTP Basic Auth方案替换为crypto/tls+cfssl证书体系,但发现net/http.Server.TLSConfig.ClientAuth设置为RequireAndVerifyClientCert后,Go 1.19默认启用TLS 1.3导致部分老旧HMI设备握手失败。最终通过MinVersion: tls.VersionTLS12显式降级,并编写tls12-compat-tester工具批量验证237台现场终端。
持续交付流水线的可观测性盲区
Jenkins Pipeline中Go服务构建阶段未注入-ldflags="-X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)",导致生产环境无法追溯二进制文件构建时间。更严重的是,go test -race未集成到CI,致使某批次焊缝轨迹补偿算法在高并发场景下出现data race on shared slice,造成3台机器人运动轨迹偏移超0.15mm。
