CGO编程实践指南
来源说明
本文档基于《Go语言高级编程(第2版)》(作者:柴树杉、曹春晖)第2章内容整理而成
原文版权归属原作者,本文档为学习笔记,仅供参考使用
目录
1. 快速入门
1.1 Hello CGO
CGO允许Go代码调用C语言函数,这是Go与C语言互操作的核心机制。
最简单的CGO程序:
package main
import "C"
// #include <stdio.h>
func main() {
    C.puts(C.CString("Hello, CGO\n"))
}
编译和运行:
go run hello.go
这段代码通过 import "C" 启用CGO特性,包含C语言的stdio.h头文件,使用C.CString()将Go字符串转换为C字符串,然后调用C的puts()函数。
1.2 自定义C函数
第一步:编写C语言函数
// hello.c
#include <stdio.h>
void SayHello(const char* s) {
    puts(s);
}
第二步:在Go中调用
package main
/*
#include "hello.c"
*/
import "C"
func main() {
    C.SayHello(C.CString("Hello, World\n"))
}
1.3 导出Go函数为C函数
CGO不仅可以在Go中调用C,还可以将Go函数导出给C调用。
从Go导出到C:
package main
/*
#include <stdio.h>
*/
import "C"
//export SayHello
func SayHello(s *C.char) {
    C.puts(s)
}
func main() {
    C.SayHello(C.CString("Hello from Go\n"))
}
注意:
- 使用 //export SayHello导出函数
- Go 1.10引入_GoString_类型表示Go字符串
- 这个调用链:Go的main() → CGO桥接函数 → Go的SayHello() → C的puts()
2. CGO基础
2.1 环境配置
前置要求:
- 安装C/C++编译工具链(macOS/Linux: GCC, Windows: MinGW)
- 设置CGO_ENABLED=1(默认启用,交叉编译需手动设置)
启用CGO:
package main
// #include <stdio.h>
// #include <math.h>
/*
// C代码注释在这里
void customFunction() {
    printf("Custom C function\n");
}
*/
import "C"  // 必须单独一行
func main() {
    C.customFunction()
}
2.2 CGO语法规则
关键点:
- import "C"必须单独一行,不能和其他包一起导入
- import "C"前面的注释是特殊语法,可以写C代码
- 包含头文件后,所有C元素都加入虚拟的"C"包中
- 可添加.c、.cpp、.h、.hpp文件到包目录
2.3 类型转换
Go的强类型要求:
func callC(v int) {
    C.printint(C.int(v))  // 必须转换类型
    // C.printint(v)      // 错误!不能直接传Go类型
}
规则:
- CGO参数类型必须与声明一致
- 传参前用虚拟C包的转换函数转换
- 不能直接传入Go变量类型
2.4 #cgo 指令
基本用法:
/*
#cgo CFLAGS: -I./include
#cgo LDFLAGS: -L${SRCDIR}/lib -lmylib
#include "myheader.h"
*/
import "C"
条件编译:
/*
#cgo linux CFLAGS: -DLINUX
#cgo darwin CFLAGS: -DDARWIN
#cgo windows CFLAGS: -DWINDOWS
*/
import "C"
平台特定:
/*
#cgo linux,386 LDFLAGS: -L/usr/lib/i386-linux-gnu
#cgo !windows LDFLAGS: -lm
*/
import "C"
重要提示:
- 头文件检索可以是相对路径
- 库文件检索必须是绝对路径
- 使用${SRCDIR}表示当前包目录
2.5 构建标签
// +build linux
package main
3. 类型转换
3.1 数值类型转换
C语言基础类型:
| C类型 | CGO类型 | Go类型 | 
|---|---|---|
| char | C.char | byte | 
| signed char | C.schar | int8 | 
| unsigned char | C.uchar | uint8 | 
| short | C.short | int16 | 
| unsigned short | C.ushort | uint16 | 
| int | C.int | int32 | 
| unsigned int | C.uint | uint32 | 
| long | C.long | int64 (64位) | 
| unsigned long | C.ulong | uint64 (64位) | 
| long long | C.longlong | int64 | 
| float | C.float | float32 | 
| double | C.double | float64 | 
注意:
- C的int和long在CGO中固定为4字节
- size_t视为- uint类型
- 复杂类型推荐使用typedef
3.2 stdint.h 类型
使用stdint.h可获得一致的类型大小:
// C代码
#include <stdint.h>
| C类型 | CGO类型 | Go类型 | 
|---|---|---|
| int8_t | C.int8_t | int8 | 
| uint16_t | C.uint16_t | uint16 | 
| int32_t | C.int32_t | int32 | 
| uint64_t | C.uint64_t | uint64 | 
示例:
var x C.uint16_t = 42  // 等价于 unsigned short
3.3 复杂类型
结构体:
/*
struct Point {
    int x;
    int y;
};
*/
import "C"
func usePoint() {
    var p C.struct_Point
    p.x = 10
    p.y = 20
}
规则:
- 使用C.struct_xxx访问C结构体
- 内存对齐遵循C规则(32位按32位对齐,64位按64位对齐)
- 特殊对齐的结构体无法访问
- 成员名与Go关键字冲突时,加下划线_
- 位字段无法直接访问,需在C中定义辅助函数
- 零长数组成员无法直接访问
联合类型:
/*
union Data {
    int i;
    float f;
};
*/
import "C"
// 联合类型会转换为字节数组
var data C.union_Data
处理联合的3种方法:
- 在C中定义辅助函数(推荐)
- 使用encoding/binary手动解码(注意字节序)
- 使用unsafe强制转换(性能最好)
枚举类型:
/*
enum Color {
    RED,
    GREEN,
    BLUE
};
*/
import "C"
func useEnum() {
    var c C.enum_Color = C.RED
}
3.4 指针转换
基本指针转换:
var p *C.int = (*C.int)(unsafe.Pointer(ptr))
X类型指针到Y类型指针:
// 使用unsafe.Pointer作为中介
var x *X
var y *Y = (*Y)(unsafe.Pointer(x))
数值类型到指针:
// int32 → char*
var addr int32 = 0x12345678
var p *C.char = (*C.char)(unsafe.Pointer(uintptr(addr)))
切片类型转换:
// []X → []Y
var xslice []X
var yslice []Y = *(*[]Y)(unsafe.Pointer(&xslice))
4. 函数调用
4.1 调用C函数
package main
/*
#include <stdlib.h>
#include <errno.h>
static int printint(int v) {
    printf("值: %d\n", v);
    return 0;
}
*/
import "C"
func main() {
    C.printint(C.int(42))
}
4.2 errno 支持
CGO对errno.h的errno宏有特殊支持:
/*
#include <errno.h>
#include <stdlib.h>
static char* getError() {
    errno = ENOENT;  // 设置errno
    return NULL;
}
*/
import "C"
func main() {
    result, err := C.getError()
    if err != nil {
        fmt.Printf("错误: %v\n", err)  // 打印errno
    }
}
规则:
- C函数如果有两个返回值,第二个是errno错误状态
4.3 导出Go函数
导出给C调用:
package main
//export Add
func Add(a, b C.int) C.int {
    return a + b
}
func main() {
    result := C.Add(C.int(3), C.int(5))
    C.printf(C.CString("结果: %d\n"), result)
}
重要限制:
- 函数参数和返回值必须是C友好类型
- 返回值不能直接或间接包含Go内存空间指针
- 导出函数不能支持const参数
- 不能是可变参数函数
- 不同Go包的同名导出函数在链接阶段会冲突
5. 内存模型
5.1 C和Go内存的差异
核心差异:
- C内存:分配后地址稳定
- Go内存:栈动态伸缩,地址可能移动
如果C持有移动前的Go指针,访问时会导致崩溃。
5.2 Go内存传递给C
CGO的安全规则:
/*
#include <stdio.h>
*/
import "C"
import "unsafe"
func printString(s string) {
    // CGO保证:在C函数返回前,传入的Go内存不会移动
    C.printf(C.CString("%s\n"), C.CString(s))
    defer C.free(unsafe.Pointer(C.CString(s)))
}
要点:
- CGO确保在C函数返回前Go内存不移动
- 但不能保存到临时变量再传入
- 长时间运行的C函数需要谨慎处理Go内存
5.3 cgo.Handle 方式(Go 1.17+)
package main
/*
#include <stdlib.h>
void printString(const char* s);
*/
import "C"
import (
    "runtime/cgo"
    "unsafe"
)
//export NewGoString
func NewGoString(s *C.char) C.uintptr_t {
    goStr := C.GoString(s)
    handle := cgo.NewHandle(goStr)
    return C.uintptr_t(handle)
}
//export PrintGoString
func PrintGoString(handle C.uintptr_t) {
    goStr := cgo.Handle(handle).Value().(string)
    C.printString(C.CString(goStr))
}
//export DeleteGoString
func DeleteGoString(handle C.uintptr_t) {
    cgo.Handle(handle).Delete()
}
原理:
- 将Go对象映射为整数id
- 即使栈伸缩,只要id稳定就能访问
- 适合长期持有Go对象
5.4 C内存返回给Go
默认检查:
- Go运行时检查返回的内存是否由Go分配
- 如果是,会抛出运行时异常
正确的做法:
// C代码:在C中分配内存
char* allocateBuffer(int size) {
    return (char*)malloc(size);
}
void freeBuffer(char* buf) {
    free(buf);
}
// Go代码
func getBuffer() []byte {
    buf := C.allocateBuffer(1024)
    // 转换为Go切片(需要知道长度)
    slice := (*[1 << 31]byte)(unsafe.Pointer(buf))[:1024:1024]
    return slice
}
5.5 runtime.Pinner(Go 1.21+)
package main
/*
#include <stdio.h>
*/
import "C"
import (
    "runtime"
    "unsafe"
)
func pinGoMemory() {
    goData := make([]byte, 1024)
    
    var pinner runtime.Pinner
    pinner.Pin(&goData[0])  // 钉住Go内存
    
    // C函数可以安全访问
    C.processData((*C.char)(unsafe.Pointer(&goData[0])), C.int(len(goData)))
    
    pinner.Unpin()
}
特点:
- 阻止Go垃圾收集器移动对象
- 临时使用,用完需Unpin
- 适合短期访问场景
6. C++类封装(重点)
这是CGO最复杂也最重要的部分。因为C++没有统一ABI,所以不能直接用CGO调用C++类,需要通过C语言接口作为桥梁。
6.1 C++类到Go对象
总体思路:
- 用纯C函数封装C++类
- 通过CGO映射为Go函数
- 创建Go封装对象
步骤1:准备C++类
// my_buffer.h
#include <string>
struct MyBuffer {
    std::string* s_;
    
    MyBuffer(int size) {
        this->s_ = new std::string(size, char('\0'));
    }
    
    ~MyBuffer() {
        delete this->s_;
    }
    
    int Size() const {
        return this->s_->size();
    }
    
    char* Data() {
        return (char*)this->s_->data();
    }
};
特点:
- 用std::string实现缓存
- 必须用new/delete(不提供复制构造函数)
- 不能值传递
步骤2:C语言接口封装
设计C接口:
// my_buffer_capi.h
typedef struct MyBuffer_T MyBuffer_T;
MyBuffer_T* NewMyBuffer(int size);
void DeleteMyBuffer(MyBuffer_T* p);
char* MyBuffer_Data(MyBuffer_T* p);
int MyBuffer_Size(MyBuffer_T* p);
实现C封装:
// my_buffer_capi.cc
#include "./my_buffer.h"
extern "C" {
    #include "./my_buffer_capi.h"
}
struct MyBuffer_T : MyBuffer {
    MyBuffer_T(int size) : MyBuffer(size) {}
    ~MyBuffer_T() {}
};
MyBuffer_T* NewMyBuffer(int size) {
    auto p = new MyBuffer_T(size);
    return p;
}
void DeleteMyBuffer(MyBuffer_T* p) {
    delete p;
}
char* MyBuffer_Data(MyBuffer_T* p) {
    return p->Data();
}
int MyBuffer_Size(MyBuffer_T* p) {
    return p->Size();
}
关键点:
- 用extern "C"确保C链接
- MyBuffer_T继承自- MyBuffer
- 隐藏C++实现细节
步骤3:映射为Go函数
// my_buffer_capi.go
package main
/*
#cgo CXXFLAGS: -std=c++11
#include "my_buffer_capi.h"
*/
import "C"
import "unsafe"
type cgo_MyBuffer_T C.MyBuffer_T
func cgo_NewMyBuffer(size int) *cgo_MyBuffer_T {
    p := C.NewMyBuffer(C.int(size))
    return (*cgo_MyBuffer_T)(p)
}
func cgo_DeleteMyBuffer(p *cgo_MyBuffer_T) {
    C.DeleteMyBuffer((*C.MyBuffer_T)(p))
}
func cgo_MyBuffer_Data(p *cgo_MyBuffer_T) *C.char {
    return C.MyBuffer_Data((*C.MyBuffer_T)(p))
}
func cgo_MyBuffer_Size(p *cgo_MyBuffer_T) C.int {
    return C.MyBuffer_Size((*C.MyBuffer_T)(p))
}
注意:
- 用#cgo CXXFLAGS启用C++11
- 函数加cgo_前缀区分
- 基础类型保持C类型
步骤4:封装为Go对象
// my_buffer.go
package main
import "unsafe"
type MyBuffer struct {
    cptr *cgo_MyBuffer_T
}
func NewMyBuffer(size int) *MyBuffer {
    return &MyBuffer{
        cptr: cgo_NewMyBuffer(size),
    }
}
func (p *MyBuffer) Delete() {
    cgo_DeleteMyBuffer(p.cptr)
}
func (p *MyBuffer) Data() []byte {
    data := cgo_MyBuffer_Data(p.cptr)
    size := cgo_MyBuffer_Size(p.cptr)
    // 转换为Go切片
    return ((*[1 << 31]byte)(unsafe.Pointer(data)))[0:int(size):int(size)]
}
func (p *MyBuffer) Size() int {
    return int(cgo_MyBuffer_Size(p.cptr))
}
使用示例:
package main
/*
#include <stdio.h>
*/
import "C"
import "unsafe"
func main() {
    buf := NewMyBuffer(1024)
    defer buf.Delete()
    
    // 复制数据到缓冲区
    copy(buf.Data(), []byte("hello\x00"))
    
    // 用C语言打印
    C.puts((*C.char)(unsafe.Pointer(&(buf.Data()[0]))))
}
6.2 Go对象到C++类
相反方向:将Go对象导出给C++使用。
步骤1:定义Go对象
// person.go
package main
type Person struct {
    name string
    age  int
}
func NewPerson(name string, age int) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}
func (p *Person) Set(name string, age int) {
    p.name = name
    p.age = age
}
func (p *Person) Get() (name string, age int) {
    return p.name, p.age
}
步骤2:导出C语言接口
// person_capi.h
#include <stdint.h>
typedef uintptr_t person_handle_t;
person_handle_t person_new(char* name, int age);
void person_delete(person_handle_t p);
void person_set(person_handle_t p, char* name, int age);
char* person_get_name(person_handle_t p, char* buf, int size);
int person_get_age(person_handle_t p);
Go实现:
// person_capi.go
/*
#include "./person_capi.h"
*/
import "C"
import (
    "runtime/cgo"
    "unsafe"
)
//export person_new
func person_new(name *C.char, age C.int) C.person_handle_t {
    id := cgo.NewHandle(NewPerson(C.GoString(name), int(age)))
    return C.person_handle_t(id)
}
//export person_delete
func person_delete(h C.person_handle_t) {
    cgo.Handle(h).Delete()
}
//export person_set
func person_set(h C.person_handle_t, name *C.char, age C.int) {
    p := cgo.Handle(h).Value().(*Person)
    p.Set(C.GoString(name), int(age))
}
//export person_get_name
func person_get_name(h C.person_handle_t, buf *C.char, size C.int) *C.char {
    p := cgo.Handle(h).Value().(*Person)
    name, _ := p.Get()
    
    n := int(size) - 1
    bufSlice := ((*[1 << 31]byte)(unsafe.Pointer(buf)))[0:n:n]
    n = copy(bufSlice, []byte(name))
    bufSlice[n] = 0
    
    return buf
}
//export person_get_age
func person_get_age(h C.person_handle_t) C.int {
    p := cgo.Handle(h).Value().(*Person)
    _, age := p.Get()
    return C.int(age)
}
关键点:
- 用cgo.Handle映射Go对象为id
- 将id转换为person_handle_t类型
- 通过id解析Go对象
步骤3:封装为C++类
标准封装方式:
// person.h
extern "C" {
    #include "./person_capi.h"
}
struct Person {
    person_handle_t goobj_;
    
    Person(const char* name, int age) {
        this->goobj_ = person_new((char*)name, age);
    }
    
    ~Person() {
        person_delete(this->goobj_);
    }
    
    void Set(char* name, int age) {
        person_set(this->goobj_, name, age);
    }
    
    char* GetName(char* buf, int size) {
        return person_get_name(this->goobj_, buf, size);
    }
    
    int GetAge() {
        return person_get_age(this->goobj_);
    }
};
使用:
// main.cpp
#include "person.h"
#include <stdio.h>
int main() {
    auto p = new Person("gopher", 15);
    
    char buf[64];
    char* name = p->GetName(buf, sizeof(buf)-1);
    int age = p->GetAge();
    
    printf("%s, %d years old.\n", name, age);
    delete p;
    
    return 0;
}
步骤4:改进版封装
避免额外的内存分配:
// person_improved.h
extern "C" {
    #include "./person_capi.h"
}
struct Person {
    static Person* New(const char* name, int age) {
        return (Person*)person_new((char*)name, age);
    }
    
    void Delete() {
        person_delete(person_handle_t(this));
    }
    
    void Set(char* name, int age) {
        person_set(person_handle_t(this), name, age);
    }
    
    char* GetName(char* buf, int size) {
        return person_get_name(person_handle_t(this), buf, size);
    }
    
    int GetAge() {
        return person_get_age(person_handle_t(this));
    }
};
区别:
- person_handle_t直接当作C++对象
- 不需要额外的goobj_成员
- 构造函数改为静态方法New()
- 只需一次内存分配
使用:
auto p = Person::New("gopher", 15);
char buf[64];
p->GetName(buf, sizeof(buf));
p->Delete();
6.3 解放C++的this指针
Go语言方法可以绑定到任何类型:
type Int int
func (p Int) Twice() int {
    return int(p) * 2
}
func main() {
    var x = Int(42)
    fmt.Println(int(x))        // 输出: 42
    fmt.Println(x.Twice())     // 输出: 84
}
在C++中实现类似效果:
struct Int {
    int Twice() {
        const int* p = (int*)(this);
        return (*p) * 2;
    }
};
int main() {
    int x = 42;
    printf("%d\n", x);                         // 42
    printf("%d\n", ((Int*)(&x))->Twice());    // 84
    return 0;
}
原理:
- this只是一个普通指针,可以自由转换
- Int结构体只是一个"壳子",不占内存
- 编译时提供方法,运行时无额外开销
7. 性能优化
7.1 MOSN带来的优化
Go 1.21版本,朱德江优化了CGO性能:
优化内容:
- 减少线程绑定: 每个C线程只需绑定Go系统线程一次(之前每次调用都绑定)
- 性能提升: 近10倍性能提升
- 内存分配: 引入#cgo noescape和#cgo nocallback
7.2 noescape 指令
/*
#cgo noescape
void fastFunction(int* p);
*/
import "C"
func callFast() {
    var data C.int
    C.fastFunction(&data)  // data不会逃逸,栈上分配
}
效果:
- 不会逃逸的Go对象优先栈上分配
- 减少堆分配压力
- 降低GC负担
7.3 nocallback 指令
/*
#cgo nocallback
void processWithoutCallback(void* data);
*/
import "C"
效果:
- 避免Go回调导致的goroutine调度
- 适合纯计算型C函数
7.4 Go 1.24 更新
这些特性已在Go 1.24正式版可用。
8. 实践总结
8.1 最佳实践
1. 类型安全
// ✅ 正确
C.someFunction(C.int(value))
// ❌ 错误
C.someFunction(value)
2. 内存管理
// ✅ 正确:立即释放
str := C.CString("hello")
defer C.free(unsafe.Pointer(str))
C.useString(str)
3. 错误处理
// ✅ 使用errno
result, err := C.someFunction()
if err != nil {
    return fmt.Errorf("CGO调用失败: %v", err)
}
4. 资源清理
// ✅ 使用defer确保释放
resource := C.allocate()
defer C.free(unsafe.Pointer(resource))
8.2 常见陷阱
1. 类型转换错误
// ❌ 错误的类型转换
var x C.long
var y = int(x)  // 可能丢失符号位
// ✅ 正确的转换
var y = int(int64(x))
2. 指针有效期
// ❌ 危险:C持有Go指针太久
func bad() {
    ptr := &goData
    go longRunningFunction(ptr)  // 可能导致崩溃
}
// ✅ 正确:使用Handle
func good() {
    handle := cgo.NewHandle(goData)
    go longRunningFunction(handle)
}
3. 不可再入
// ⚠️ C函数不总是可再入的
// 注意:某些全局变量可能导致问题
8.3 调试技巧
1. 启用详细日志
CGO_ENABLED=1 go build -x
2. 查看生成的C代码
/*
#include "debug.h"
void debugPrint(int v) {
    printf("debug: %d\n", v);
}
*/
import "C"
3. 检查内存对齐
import "unsafe"
func checkAlignment() {
    var s C.struct_MyStruct
    size := unsafe.Sizeof(s)
    fmt.Printf("结构体大小: %d\n", size)
}
8.4 性能优化建议
1. 减少CGO调用开销
- 批量处理数据,减少调用次数
- 使用Handle避免重复绑定
2. 内存优化
- 使用noescape避免不必要的堆分配
- 注意C内存和Go内存的区别
3. 并发安全
- C函数不总是线程安全的
- 考虑加锁保护共享C资源
8.5 何时使用CGO
适合场景:
- ✅ 调用系统级C库(如OpenSSL、FFmpeg)
- ✅ 复用现有C/C++代码
- ✅ 性能关键的计算逻辑
- ✅ 需要底层硬件访问
不适合场景:
- ❌ 纯Go能实现的业务逻辑
- ❌ 频繁调用的简单函数
- ❌ 仅为了使用C语法
8.6 替代方案
1. c-shared 构建模式
go build -buildmode=c-shared -o libmy.so
2. c-archive 构建模式
go build -buildmode=c-archive -o libmy.a
3. 纯C共享库
- 完全用C实现接口
- Go调用标准C库
附录:完整示例
示例1:加密库调用(OpenSSL)
package main
/*
#cgo LDFLAGS: -lcrypto
#include <openssl/sha.h>
*/
import "C"
import (
    "fmt"
    "unsafe"
)
func sha256(data []byte) []byte {
    var result [32]byte
    C.SHA256(
        (*C.uchar)(unsafe.Pointer(&data[0])),
        C.size_t(len(data)),
        (*C.uchar)(unsafe.Pointer(&result[0])),
    )
    return result[:]
}
func main() {
    data := []byte("hello world")
    hash := sha256(data)
    fmt.Printf("%x\n", hash)
}
示例2:调用C++标准库
/*
#cgo CXXFLAGS: -std=c++11
#include <string>
#include <iostream>
static void printString(const char* s) {
    std::string str(s);
    std::cout << str << std::endl;
}
*/
import "C"
func main() {
    C.printString(C.CString("Hello from C++"))
}
示例3:Go导出为动态库
package main
import "C"
//export Add
func Add(a, b C.int) C.int {
    return a + b
}
//export ProcessData
func ProcessData(data *C.char, len C.int) *C.char {
    result := C.malloc(C.size_t(len) + 1)
    C.strcpy(result, data)
    return result
}
func main() {}  // 必须有main
go build -buildmode=c-shared -o libmath.so
总结
CGO是连接Go和C/C++世界的桥梁,掌握它需要理解:
- 类型系统: C与Go类型的对应关系
- 内存模型: C内存稳定,Go内存可移动
- 内存管理: 正确管理两边内存,避免泄漏
- 封装技巧: 通过C接口封装C++类
- 性能优化: 减少调用开销,正确使用指令
关键原则:
- 用C作为Go与C++的桥梁
- 谨慎处理内存生命周期
- 充分测试C函数的并发安全
- 优先使用Go能解决的方案
祝你在CGO编程的道路上越走越远!
版权声明
本文档为《Go语言高级编程(第2版)》(作者:柴树杉、曹春晖)的学习笔记整理。
原书信息:
- 书名:《Go语言高级编程(第2版)》
- 作者:柴树杉、曹春晖
- 出版社:电子工业出版社
本文档说明:
- 本文档基于原书内容整理,是个人学习笔记
- 原文版权归原作者和出版社所有
- 本文档仅供学习交流使用,请勿用于商业目的
- 如需引用,请标注原书来源
 
                   
                   
                   
                   
         
           
       
       
           
                 
                 
                 
                 
                 
                
               
                 
                 
                 
                 
                
               
                 
                 扫一扫
扫一扫
                     
              
             
                   1044
					1044
					
 被折叠的  条评论
		 为什么被折叠?
被折叠的  条评论
		 为什么被折叠?
		 
		  到【灌水乐园】发言
到【灌水乐园】发言                                
		 
		 
    
   
    
   
             
            


 
            