一、内存模型CGO是架接Go语言和C语言的桥梁，它使二者在二进制接口层面实现了互通，但是我们要注意因两种语言的内存模型的差异而可能引起的问题。如果在CGO处理的跨语言函数调用时涉及到了指针的传递，则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的，但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针，那么以旧指针访问Go对象时会导致程序崩溃。1、Go访问C内存C语言空间的内存是稳定的，只要不是被人为提前释放，那么在Go语言空间可以放心大胆地使用。在Go语言访问C语言内存是最简单的情形，我们在之前的例子中已经见过多次。因为Go语言实现的限制，我们无法在Go语言中创建大于2GB内存的切片。不过借助cgo技术，我们可以在C语言环境创建大于2GB的内存，然后转为Go语言的切片使用：package main /* #include <stdlib.h> void* makeslice(size_t memsize) { return malloc(memsize); } */ import "C" import "unsafe" func makeByteSlize(n int) []byte { p := C.makeslice(C.size_t(n)) return ((*[1 << 31]byte)(p))[0:n:n] } func freeByteSlice(p []byte) { C.free(unsafe.Pointer(&p[0])) } func main() { s := makeByteSlize(1<<32+1) s[len(s)-1] = 255 print(s[len(s)-1]) freeByteSlice(s) } 例子中我们通过makeByteSlize来创建大于4G内存大小的切片，从而绕过了Go语言实现的限制。而freeByteSlice辅助函数则用于释放从C语言函数创建的切片。因为C语言内存空间是稳定的，基于C语言内存构造的切片也是绝对稳定的，不会因为Go语言栈的变化而被移动。2、C临时访问传入的Go内存cgo之所以存在的一大因素是为了方便在Go语言中接纳吸收过去几十年来使用C/C++语言软件构建的大量的软件资源。C/C++很多库都是需要通过指针直接处理传入的内存数据的，因此cgo中也有很多需要将Go内存传入C语言函数的应用场景。假设一个极端场景：我们将一块位于某goroutinue的栈上的Go语言内存传入了C语言函数后，在此C语言函数执行期间，此goroutinue的栈因为空间不足的原因发生了扩展，也就是导致了原来的Go语言内存被移动到了新的位置。但是此时此刻C语言函数并不知道该Go语言内存已经移动了位置，仍然用之前的地址来操作该内存——这将将导致内存越界。以上是一个推论（真实情况有些差异），也就是说C访问传入的Go内存可能是不安全的！当然有RPC远程过程调用的经验的用户可能会考虑通过完全传值的方式处理：借助C语言内存稳定的特性，在C语言空间先开辟同样大小的内存，然后将Go的内存填充到C的内存空间；返回的内存也是如此处理。下面的例子是这种思路的具体实现：package main /* void printString(const char* s) { printf("%s", s); } */ import "C" func printString(s string) { cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) C.printString(cs) } func main() { s := "hello" printString(s) } 在需要将Go的字符串传入C语言时，先通过C.CString将Go语言字符串对应的内存数据复制到新创建的C语言内存空间上。上面例子的处理思路虽然是安全的，但是效率极其低下（因为要多次分配内存并逐个复制元素），同时也极其繁琐。为了简化并高效处理此种向C语言传入Go语言内存的问题，cgo针对该场景定义了专门的规则：在CGO调用的C语言函数返回前，cgo保证传入的Go语言内存在此期间不会发生移动，C语言函数可以大胆地使用Go语言的内存！根据新的规则我们可以直接传入Go字符串的内存：package main /* #include<stdio.h> void printString(const char* s, int n) { int i; for(i = 0; i < n; i++) { putchar(s[i]); } putchar('\n'); } */ import "C" func printString(s string) { p := (*reflect.StringHeader)(unsafe.Pointer(&s)) C.printString((*C.char)(unsafe.Pointer(p.Data)), C.int(len(s))) } func main() { s := "hello" printString(s) } 现在的处理方式更加直接，且避免了分配额外的内存。完美的解决方案！任何完美的技术都有被滥用的时候，CGO的这种看似完美的规则也是存在隐患的。我们假设调用的C语言函数需要长时间运行，那么将会导致被他引用的Go语言内存在C语言返回前不能被移动，从而可能间接地导致这个Go内存栈对应的goroutine不能动态伸缩栈内存，也就是可能导致这个goroutine被阻塞。因此，在需要长时间运行的C语言函数（特别是在纯CPU运算之外，还可能因为需要等待其它的资源而需要不确定时间才能完成的函数），需要谨慎处理传入的Go语言内存。不过需要小心的是在取得Go内存后需要马上传入C语言函数，不能保存到临时变量后再间接传入C语言函数。因为CGO只能保证在C函数调用之后被传入的Go语言内存不会发生移动，它并不能保证在传入C函数之前内存不发生变化。以下代码是错误的：// 错误的代码 tmp := uintptr(unsafe.Pointer(&x)) pb := (*int16)(unsafe.Pointer(tmp)) *pb = 42 因为tmp并不是指针类型，在它获取到Go对象地址之后x对象可能会被移动，但是因为不是指针类型，所以不会被Go语言运行时更新成新内存的地址。在非指针类型的tmp保持Go对象的地址，和在C语言环境保持Go对象的地址的效果是一样的：如果原始的Go对象内存发生了移动，Go语言运行时并不会同步更新它们。3、C长期持有Go指针对象作为一个Go程序员在使用CGO时潜意识会认为总是Go调用C函数。其实CGO中，C语言函数也可以回调Go语言实现的函数。特别是我们可以用Go语言写一个动态库，导出C语言规范的接口给其它用户调用。当C语言函数调用Go语言函数的时候，C语言函数就成了程序的调用方，Go语言函数返回的Go对象内存的生命周期也就自然超出了Go语言运行时的管理。简言之，我们不能在C语言函数中直接使用Go语言对象的内存。虽然Go语言禁止在C语言函数中长期持有Go指针对象，但是这种需求是切实存在的。如果需要在C语言中访问Go语言内存对象，我们可以将Go语言内存对象在Go语言空间映射为一个int类型的id，然后通过此id来间接访问和控制Go语言对象。以下代码用于将Go对象映射为整数类型的ObjectId，用完之后需要手工调用free方法释放该对象ID：package main import "sync" type ObjectId int32 var refs struct { sync.Mutex objs map[ObjectId]interface{} next ObjectId } func init() { refs.Lock() defer refs.Unlock() refs.objs = make(map[ObjectId]interface{}) refs.next = 1000 } func NewObjectId(obj interface{}) ObjectId { refs.Lock() defer refs.Unlock() id := refs.next refs.next++ refs.objs[id] = obj return id } func (id ObjectId) IsNil() bool { return id == 0 } func (id ObjectId) Get() interface{} { refs.Lock() defer refs.Unlock() return refs.objs[id] } func (id *ObjectId) Free() interface{} { refs.Lock() defer refs.Unlock() obj := refs.objs[*id] delete(refs.objs, *id) *id = 0 return obj } 我们通过一个map来管理Go语言对象和id对象的映射关系。其中NewObjectId用于创建一个和对象绑定的id，而id对象的方法可用于解码出原始的Go对象，也可以用于结束id和原始Go对象的绑定。下面一组函数以C接口规范导出，可以被C语言函数调用：package main /* extern char* NewGoString(char* ); extern void FreeGoString(char* ); extern void PrintGoString(char* ); static void printString(const char* s) { char* gs = NewGoString(s); PrintGoString(gs); FreeGoString(gs); } */ import "C" //export NewGoString func NewGoString(s *C.char) *C.char { gs := C.GoString(s) id := NewObjectId(gs) return (*C.char)(unsafe.Pointer(uintptr(id))) } //export FreeGoString func FreeGoString(p *C.char) { id := ObjectId(uintptr(unsafe.Pointer(p))) id.Free() } //export PrintGoString func PrintGoString(s *C.char) { id := ObjectId(uintptr(unsafe.Pointer(p))) gs := id.Get().(string) print(gs) } func main() { C.printString("hello") } 在printString函数中，我们通过NewGoString创建一个对应的Go字符串对象，返回的其实是一个id，不能直接使用。我们借助PrintGoString函数将id解析为Go语言字符串后打印。该字符串在C语言函数中完全跨越了Go语言的内存管理，在PrintGoString调用前即使发生了栈伸缩导致的Go字符串地址发生变化也依然可以正常工作，因为该字符串对应的id是稳定的，在Go语言空间通过id解码得到的字符串也就是有效的。4、导出C函数不能返回Go内存在Go语言中，Go是从一个固定的虚拟地址空间分配内存。而C语言分配的内存则不能使用Go语言保留的虚拟内存空间。在CGO环境，Go语言运行时默认会检查导出返回的内存是否是由Go语言分配的，如果是则会抛出运行时异常。/* extern int* getGoPtr(); static void Main() { int* p = getGoPtr(); *p = 42; } */ import "C" func main() { C.Main() } //export getGoPtr func getGoPtr() *C.int { return new(C.int) } 其中getGoPtr返回的虽然是C语言类型的指针，但是内存本身是从Go语言的new函数分配，也就是由Go语言运行时统一管理的内存。然后我们在C语言的Main函数中调用了getGoPtr函数，此时默认将发送运行时异常：$ go run main.go panic: runtime error: cgo result has Go pointer goroutine 1 [running]: main._cgoexpwrap_cfb3840e3af2_getGoPtr.func1(0xc420051dc0) command-line-arguments/_obj/_cgo_gotypes.go:60 +0x3a main._cgoexpwrap_cfb3840e3af2_getGoPtr(0xc420016078) command-line-arguments/_obj/_cgo_gotypes.go:62 +0x67 main._Cfunc_Main() command-line-arguments/_obj/_cgo_gotypes.go:43 +0x41 main.main() /Users/chai/go/src/github.com/chai2010 \ /advanced-go-programming-book/examples/ch2-xx \ /return-go-ptr/main.go:17 +0x20 exit status 2 异常说明cgo函数返回的结果中含有Go语言分配的指针。指针的检查操作发生在C语言版的getGoPtr函数中，它是由cgo生成的桥接C语言和Go语言的函数。下面是cgo生成的C语言版本getGoPtr函数的具体细节（在cgo生成的_cgo_export.c文件定义）：int* getGoPtr() { __SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done(); struct { int* r0; } __attribute__((__packed__)) a; _cgo_tsan_release(); crosscall2(_cgoexp_95d42b8e6230_getGoPtr, &a, 8, _cgo_ctxt); _cgo_tsan_acquire(); _cgo_release_context(_cgo_ctxt); return a.r0; } 其中_cgo_tsan_acquire是从LLVM项目移植过来的内存指针扫描函数，它会检查cgo函数返回的结果是否包含Go指针。需要说明的是，cgo默认对返回结果的指针的检查是有代价的，特别是cgo函数返回的结果是一个复杂的数据结构时将花费更多的时间。如果已经确保了cgo函数返回的结果是安全的话，可以通过设置环境变量GODEBUG=cgocheck=0来关闭指针检查行为。$ GODEBUG=cgocheck=0 go run main.go 关闭cgocheck功能后再运行上面的代码就不会出现上面的异常的。但是要注意的是，如果C语言使用期间对应的内存被Go运行时释放了，将会导致更严重的崩溃问题。cgocheck默认的值是1，对应一个简化版本的检测，如果需要完整的检测功能可以将cgocheck设置为2。二、C++类包装CGO是C语言和Go语言之间的桥梁，原则上无法直接支持C++的类。CGO不支持C++语法的根本原因是C++至今为止还没有一个二进制接口规范(ABI)。一个C++类的构造函数在编译为目标文件时如何生成链接符号名称、方法在不同平台甚至是C++的不同版本之间都是不一样的。但是C++是兼容C语言，所以我们可以通过增加一组C语言函数接口作为C++类和CGO之间的桥梁，这样就可以间接地实现C++和Go之间的互联。当然，因为CGO只支持C语言中值类型的数据类型，所以我们是无法直接使用C++的引用参数等特性的。1、C++类到Go语言对象实现C++类到Go语言对象的包装需要经过以下几个步骤：首先是用纯C函数接口包装该C++类；其次是通过CGO将纯C函数接口映射到Go函数；最后是做一个Go包装对象，将C++类到方法用Go对象的方法实现。1.1、准备一个C++类为了演示简单，我们基于std::string做一个最简单的缓存类MyBuffer。除了构造函数和析构函数之外，只有两个成员函数分别是返回底层的数据指针和缓存的大小。因为是二进制缓存，所以我们可以在里面中放置任意数据。// 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(); } }; 我们在构造函数中指定缓存的大小并分配空间，在使用完之后通过析构函数释放内部分配的内存空间。下面是简单的使用方式：int main() { auto pBuf = new MyBuffer(1024); auto data = pBuf->Data(); auto size = pBuf->Size(); delete pBuf; } 为了方便向C语言接口过渡，在此处我们故意没有定义C++的拷贝构造函数。我们必须以new和delete来分配和释放缓存对象，而不能以值风格的方式来使用。1.2、用纯C函数接口封装C++类如果要将上面的C++类用C语言函数接口封装，我们可以从使用方式入手。我们可以将new和delete映射为C语言函数，将对象的方法也映射为C语言函数。在C语言中我们期望MyBuffer类可以这样使用：int main() { MyBuffer* pBuf = NewMyBuffer(1024); char* data = MyBuffer_Data(pBuf); auto size = MyBuffer_Size(pBuf); DeleteMyBuffer(pBuf); } 先从C语言接口用户的角度思考需要什么样的接口，然后创建my_buffer_capi.h头文件接口规范：// 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++的MyBuffer类定义这些C语言包装函数。我们创建对应的my_buffer_capi.cc文件如下：// 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(); } 因为头文件my_buffer_capi.h是用于CGO，必须是采用C语言规范的名字修饰规则。在C++源文件包含时需要用extern "C"语句说明。另外MyBuffer_T的实现只是从MyBuffer继承的类，这样可以简化包装代码的实现。同时和CGO通信时必须通过MyBuffer_T指针，我们无法将具体的实现暴露给CGO，因为实现中包含了C++特有的语法，CGO无法识别C++特性。1.3、将纯C接口函数转为Go函数将纯C函数包装为对应的Go函数的过程比较简单。需要注意的是，因为我们的包中包含C++11的语法，因此需要通过#cgo CXXFLAGS: -std=c++11打开C++11的选项。// my_buffer_capi.go package main /* #cgo CXXFLAGS: -std=c++11 #include "my_buffer_capi.h" */ import "C" 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)) } 为了区分，我们在Go中的每个类型和函数名称前面增加了cgo_前缀，比如cgo_MyBuffer_T是对应C中的MyBuffer_T类型。为了处理简单，在包装纯C函数到Go函数时，除了cgo_MyBuffer_T类型外，对输入参数和返回值的基础类型，我们依然是用的C语言的类型。1.4、包装为Go对象在将纯C接口包装为Go函数之后，我们就可以很容易地基于包装的Go函数构造出Go对象来。因为cgo_MyBuffer_T是从C语言空间导入的类型，它无法定义自己的方法，因此我们构造了一个新的MyBuffer类型，里面的成员持有cgo_MyBuffer_T指向的C语言缓存对象。// 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) return ((*[1 << 31]byte)(unsafe.Pointer(data)))[0:int(size):int(size)] } 同时，因为Go语言的切片本身含有长度信息，我们将cgo_MyBuffer_Data和cgo_MyBuffer_Size两个函数合并为MyBuffer.Data方法，它返回一个对应底层C语言缓存空间的切片。现在我们就可以很容易在Go语言中使用包装后的缓存对象了（底层是基于C++的std::string实现）：package main //#include <stdio.h> import "C" import "unsafe" func main() { buf := NewMyBuffer(1024) defer buf.Delete() copy(buf.Data(), []byte("hello\x00")) C.puts((*C.char)(unsafe.Pointer(&(buf.Data()[0])))) } 例子中，我们创建了一个1024字节大小的缓存，然后通过copy函数向缓存填充了一个字符串。为了方便C语言字符串函数处理，我们在填充字符串的默认用'\0'表示字符串结束。最后我们直接获取缓存的底层数据指针，用C语言的puts函数打印缓存的内容。2、Go语言对象到C++类要实现Go语言对象到C++类的包装需要经过以下几个步骤：首先是将Go对象映射为一个id；然后基于id导出对应的C接口函数；最后是基于C接口函数包装为C++对象。2.1、构造一个Go对象为了便于演示，我们用Go语言构建了一个Person对象，每个Person可以有名字和年龄信息：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 } Person对象如果想要在C/C++中访问，需要通过cgo导出C接口来访问。2.2、导出C接口我们前面仿照C++对象到C接口的过程，也抽象一组C接口描述Person对象。创建一个person_capi.h文件，对应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语言中实现这一组C函数。需要注意的是，通过CGO导出C函数时，输入参数和返回值类型都不支持const修饰，同时也不支持可变参数的函数类型。同时如内存模式一节所述，我们无法在C/C++中直接长期访问Go内存对象。因此我们使用前一节所讲述的技术将Go对象映射为一个整数id。下面是person_capi.go文件，对应C接口函数的实现：// person_capi.go package main //#include "./person_capi.h" import "C" import "unsafe" //export person_new func person_new(name *C.char, age C.int) C.person_handle_t { id := NewObjectId(NewPerson(C.GoString(name), int(age))) return C.person_handle_t(id) } //export person_delete func person_delete(h C.person_handle_t) { ObjectId(h).Free() } //export person_set func person_set(h C.person_handle_t, name *C.char, age C.int) { p := ObjectId(h).Get().(*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 := ObjectId(h).Get().(*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 := ObjectId(h).Get().(*Person) _, age := p.Get() return C.int(age) } 在创建Go对象后，我们通过NewObjectId将Go对应映射为id。然后将id强制转义为person_handle_t类型返回。其它的接口函数则是根据person_handle_t所表示的id，让根据id解析出对应的Go对象。2.3、封装C++对象有了C接口之后封装C++对象就比较简单了。常见的做法是新建一个Person类，里面包含一个person_handle_t类型的成员对应真实的Go对象，然后在Person类的构造函数中通过C接口创建Go对象，在析构函数中通过C接口释放Go对象。下面是采用这种技术的实现：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_); } } 包装后我们就可以像普通C++类那样使用了：#include "person.h" #include <stdio.h> int main() { auto p = new Person("gopher", 10); 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; } 2.4、封装C++对象改进在前面的封装C++对象的实现中，每次通过new创建一个Person实例需要进行两次内存分配：一次是针对C++版本的Person，再一次是针对Go语言版本的Person。其实C++版本的Person内部只有一个person_handle_t类型的id，用于映射Go对象。我们完全可以将person_handle_t直接当中C++对象来使用。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类中增加了一个叫New静态成员函数，用于创建新的Person实例。在New函数中通过调用person_new来创建Person实例，返回的是person_handle_t类型的id，我们将其强制转型作为Person*类型指针返回。在其它的成员函数中，我们通过将this指针再反向转型为person_handle_t类型，然后通过C接口调用对应的函数。3、彻底解放C++的this指针熟悉Go语言的用法会发现Go语言中方法是绑定到类型的。比如我们基于int定义一个新的Int类型，就可以有自己的方法：type Int int func (p Int) Twice() int { return int(p)*2 } func main() { var x = Int(42) fmt.Println(int(x)) fmt.Println(x.Twice()) } 这样就可以在不改变原有数据底层内存结构的前提下，自由切换int和Int类型来使用变量。而在C++中要实现类似的特性，一般会采用以下实现：class Int { int v_; Int(v int) { this.v_ = v; } int Twice() const{ return this.v_*2; } }; int main() { Int v(42); printf("%d\n", v); // error printf("%d\n", v.Twice()); } 新包装后的Int类虽然增加了Twice方法，但是失去了自由转回int类型的权利。这时候不仅连printf都无法输出Int本身的值，而且也失去了int类型运算的所有特性。这就是C++构造函数的邪恶之处：以失去原有的一切特性的代价换取class的施舍。造成这个问题的根源是C++中this被固定为class的指针类型了。我们重新回顾下this在Go语言中的本质：func (this Int) Twice() int func Int_Twice(this Int) int 在Go语言中，和this有着相似功能的类型接收者参数其实只是一个普通的函数参数，我们可以自由选择值或指针类型。如果以C语言的角度来思考，this也只是一个普通的void*类型的指针，我们可以随意自由地将this转换为其它类型。struct Int { int Twice() { const int* p = (int*)(this); return (*p) * 2; } }; int main() { int x = 42; printf("%d\n", x); printf("%d\n", ((Int*)(&x))->Twice()); return 0; } 这样我们就可以通过将int类型指针强制转为Int类型指针，代替通过默认的构造函数后new来构造Int对象。在Twice函数的内部，以相反的操作将this指针转回int类型的指针，就可以解析出原有的int类型的值了。这时候Int类型只是编译时的一个壳子，并不会在运行时占用额外的空间。因此C++的方法其实也可以用于普通非class类型，C++到普通成员函数其实也是可以绑定到类型的。只有纯虚方法是绑定到对象，那就是接口。三、静态库和动态库CGO在使用C/C++资源的时候一般有三种形式：直接使用源码；链接静态库；链接动态库。直接使用源码就是在import "C"之前的注释部分包含C代码，或者在当前包中包含C/C++源文件。链接静态库和动态库的方式比较类似，都是通过在LDFLAGS选项指定要链接的库方式链接。本节我们主要关注在CGO中如何使用静态库和动态库相关的问题。1、使用C静态库如果CGO中引入的C/C++资源有代码而且代码规模也比较小，直接使用源码是最理想的方式，但很多时候我们并没有源代码，或者从C/C++源代码开始构建的过程异常复杂，这种时候使用C静态库也是一个不错的选择。静态库因为是静态链接，最终的目标程序并不会产生额外的运行时依赖，也不会出现动态库特有的跨运行时资源管理的错误。不过静态库对链接阶段会有一定要求：静态库一般包含了全部的代码，里面会有大量的符号，如果不同静态库之间出现了符号冲突则会导致链接的失败。我们先用纯C语言构造一个简单的静态库。我们要构造的静态库名叫number，库中只有一个number_add_mod函数，用于表示数论中的模加法运算。number库的文件都在number目录下。number/number.h头文件只有一个纯C语言风格的函数声明：int number_add_mod(int a, int b, int mod); number/number.c对应函数的实现：#include "number.h" int number_add_mod(int a, int b, int mod) { return (a+b)%mod; } 因为CGO使用的是GCC命令来编译和链接C和Go桥接的代码。因此静态库也必须是GCC兼容的格式。通过以下命令可以生成一个叫libnumber.a的静态库：$ cd ./number $ gcc -c -o number.o number.c $ ar rcs libnumber.a number.o 生成libnumber.a静态库之后，我们就可以在CGO中使用该资源了。创建main.go文件如下：package main //#cgo CFLAGS: -I./number //#cgo LDFLAGS: -L${SRCDIR}/number -lnumber // //#include "number.h" import "C" import "fmt" func main() { fmt.Println(C.number_add_mod(10, 5, 12)) } 其中有两个#cgo命令，分别是编译和链接参数。CFLAGS通过-I./number将number库对应头文件所在的目录加入头文件检索路径。LDFLAGS通过-L${SRCDIR}/number将编译后number静态库所在目录加为链接库检索路径，-lnumber表示链接libnumber.a静态库。需要注意的是，在链接部分的检索路径不能使用相对路径（C/C++代码的链接程序所限制），我们必须通过cgo特有的${SRCDIR}变量将源文件对应的当前目录路径展开为绝对路径（因此在windows平台中绝对路径不能有空白符号）。因为我们有number库的全部代码，所以我们可以用go generate工具来生成静态库，或者是通过Makefile来构建静态库。因此发布CGO源码包时，我们并不需要提前构建C静态库。因为多了一个静态库的构建步骤，这种使用了自定义静态库并已经包含了静态库全部代码的Go包无法直接用go get安装。不过我们依然可以通过go get下载，然后用go generate触发静态库构建，最后才是go install来完成安装。为了支持go get命令直接下载并安装，我们C语言的#include语法可以将number库的源文件链接到当前的包。创建z_link_number_c.c文件如下：#include "./number/number.c" 然后在执行go get或go build之类命令的时候，CGO就是自动构建number库对应的代码。这种技术是在不改变静态库源代码组织结构的前提下，将静态库转化为了源代码方式引用。这种CGO包是最完美的。如果使用的是第三方的静态库，我们需要先下载安装静态库到合适的位置。然后在#cgo命令中通过CFLAGS和LDFLAGS来指定头文件和库的位置。对于不同的操作系统甚至同一种操作系统的不同版本来说，这些库的安装路径可能都是不同的，那么如何在代码中指定这些可能变化的参数呢？在Linux环境，有一个pkg-config命令可以查询要使用某个静态库或动态库时的编译和链接参数。我们可以在#cgo命令中直接使用pkg-config命令来生成编译和链接参数。而且还可以通过PKG_CONFIG环境变量定制pkg-config命令。因为不同的操作系统对pkg-config命令的支持不尽相同，通过该方式很难兼容不同的操作系统下的构建参数。不过对于Linux等特定的系统，pkg-config命令确实可以简化构建参数的管理。2、使用C动态库动态库出现的初衷是对于相同的库，多个进程可以共享同一个，以节省内存和磁盘资源。但是在磁盘和内存已经白菜价的今天，这两个作用已经显得微不足道了，那么除此之外动态库还有哪些存在的价值呢？从库开发角度来说，动态库可以隔离不同动态库之间的关系，减少链接时出现符号冲突的风险。而且对于windows等平台，动态库是跨越VC和GCC不同编译器平台的唯一的可行方式。对于CGO来说，使用动态库和静态库是一样的，因为动态库也必须要有一个小的静态导出库用于链接动态库（Linux下可以直接链接so文件，但是在Windows下必须为dll创建一个.a文件用于链接）。我们还是以前面的number库为例来说明如何以动态库方式使用。对于在macOS和Linux系统下的gcc环境，我们可以用以下命令创建number库的的动态库：$ cd number $ gcc -shared -o libnumber.so number.c 因为动态库和静态库的基础名称都是libnumber，只是后缀名不同而已。因此Go语言部分的代码和静态库版本完全一样：package main //#cgo CFLAGS: -I./number //#cgo LDFLAGS: -L${SRCDIR}/number -lnumber // //#include "number.h" import "C" import "fmt" func main() { fmt.Println(C.number_add_mod(10, 5, 12)) } 编译时GCC会自动找到libnumber.a或libnumber.so进行链接。对于windows平台，我们还可以用VC工具来生成动态库（windows下有一些复杂的C++库只能用VC构建）。我们需要先为number.dll创建一个def文件，用于控制要导出到动态库的符号。number.def文件的内容如下：LIBRARY number.dll EXPORTS number_add_mod 其中第一行的LIBRARY指明动态库的文件名，然后的EXPORTS语句之后是要导出的符号名列表。现在我们可以用以下命令来创建动态库（需要进入VC对应的x64命令行环境）。$ cl /c number.c $ link /DLL /OUT:number.dll number.obj number.def 这时候会为dll同时生成一个number.lib的导出库。但是在CGO中我们无法使用lib格式的链接库。要生成.a格式的导出库需要通过mingw工具箱中的dlltool命令完成：$ dlltool -dllname number.dll --def number.def --output-lib libnumber.a 生成了libnumber.a文件之后，就可以通过-lnumber链接参数进行链接了。需要注意的是，在运行时需要将动态库放到系统能够找到的位置。对于windows来说，可以将动态库和可执行程序放到同一个目录，或者将动态库所在的目录绝对路径添加到PATH环境变量中。对于macOS来说，需要设置DYLD_LIBRARY_PATH环境变量。而对于Linux系统来说，需要设置LD_LIBRARY_PATH环境变量。3、导出C静态库CGO不仅可以使用C静态库，也可以将Go实现的函数导出为C静态库。我们现在用Go实现前面的number库的模加法函数。创建number.go，内容如下：package main import "C" func main() {} //export number_add_mod func number_add_mod(a, b, mod C.int) C.int { return (a + b) % mod } 根据CGO文档的要求，我们需要在main包中导出C函数。对于C静态库构建方式来说，会忽略main包中的main函数，只是简单导出C函数。采用以下命令构建：$ go build -buildmode=c-archive -o number.a 在生成number.a静态库的同时，cgo还会生成一个number.h文件。number.h文件的内容如下（为了便于显示，内容做了精简）：#ifdef __cplusplus extern "C" { #endif extern int number_add_mod(int p0, int p1, int p2); #ifdef __cplusplus } #endif 其中extern "C"部分的语法是为了同时适配C和C++两种语言。核心内容是声明了要导出的number_add_mod函数。然后我们创建一个_test_main.c的C文件用于测试生成的C静态库（用下划线作为前缀名是让为了让go build构建C静态库时忽略这个文件）：#include "number.h" #include <stdio.h> int main() { int a = 10; int b = 5; int c = 12; int x = number_add_mod(a, b, c); printf("(%d+%d)%%%d = %d\n", a, b, c, x); return 0; } 通过以下命令编译并运行：$ gcc -o a.out _test_main.c number.a $ ./a.out 使用CGO创建静态库的过程非常简单。4、导出C动态库CGO导出动态库的过程和静态库类似，只是将构建模式改为c-shared，输出文件名改为number.so而已：$ go build -buildmode=c-shared -o number.so _test_main.c文件内容不变，然后用以下命令编译并运行：$ gcc -o a.out _test_main.c number.so $ ./a.out 5、导出非main包的函数通过go help buildmode命令可以查看C静态库和C动态库的构建说明：-buildmode=c-archive Build the listed main package, plus all packages it imports, into a C archive file. The only callable symbols will be those functions exported using a cgo //export comment. Requires exactly one main package to be listed. -buildmode=c-shared Build the listed main package, plus all packages it imports, into a C shared library. The only callable symbols will be those functions exported using a cgo //export comment. Requires exactly one main package to be listed. 文档说明导出的C函数必须是在main包导出，然后才能在生成的头文件包含声明的语句。但是很多时候我们可能更希望将不同类型的导出函数组织到不同的Go包中，然后统一导出为一个静态库或动态库。要实现从是从非main包导出C函数，或者是多个包导出C函数（因为只能有一个main包），我们需要自己提供导出C函数对应的头文件（因为CGO无法为非main包的导出函数生成头文件）。假设我们先创建一个number子包，用于提供模加法函数：package number import "C" //export number_add_mod func number_add_mod(a, b, mod C.int) C.int { return (a + b) % mod } 然后是当前的main包：package main import "C" import ( "fmt" _ "./number" ) func main() { println("Done") } //export goPrintln func goPrintln(s *C.char) { fmt.Println("goPrintln:", C.GoString(s)) } 其中我们导入了number子包，在number子包中有导出的C函数number_add_mod，同时我们在main包也导出了goPrintln函数。通过以下命令创建C静态库：$ go build -buildmode=c-archive -o main.a 这时候在生成main.a静态库的同时，也会生成一个main.h头文件。但是main.h头文件中只有main包中导出的goPrintln函数的声明，并没有number子包导出函数的声明。其实number_add_mod函数在生成的C静态库中是存在的，我们可以直接使用。创建_test_main.c测试文件如下：#include <stdio.h> void goPrintln(char*); int number_add_mod(int a, int b, int mod); int main() { int a = 10; int b = 5; int c = 12; int x = number_add_mod(a, b, c); printf("(%d+%d)%%%d = %d\n", a, b, c, x); goPrintln("done"); return 0; } 我们并没有包含CGO自动生成的main.h头文件，而是通过手工方式声明了goPrintln和number_add_mod两个导出函数。这样我们就实现了从多个Go包导出C函数了。四、编译和链接参数编译和链接参数是每一个C/C++程序员需要经常面对的问题。构建每一个C/C++应用均需要经过编译和链接两个步骤，CGO也是如此。1、编译参数：CFLAGS/CPPFLAGS/CXXFLAGS编译参数主要是头文件的检索路径，预定义的宏等参数。理论上来说C和C++是完全独立的两个编程语言，它们可以有着自己独立的编译参数。但是因为C++语言对C语言做了深度兼容，甚至可以将C++理解为C语言的超集，因此C和C++语言之间又会共享很多编译参数。因此CGO提供了CFLAGS/CPPFLAGS/CXXFLAGS三种参数，其中CFLAGS对应C语言编译参数(以.c后缀名)、 CPPFLAGS对应C/C++ 代码编译参数(.c,.cc,.cpp,.cxx)、CXXFLAGS对应纯C++编译参数(.cc,.cpp,*.cxx)。2、链接参数：LDFLAGS链接参数主要包含要链接库的检索目录和要链接库的名字。因为历史遗留问题，链接库不支持相对路径，我们必须为链接库指定绝对路径。 cgo 中的 ${SRCDIR} 为当前目录的绝对路径。经过编译后的C和C++目标文件格式是一样的，因此LDFLAGS对应C/C++共同的链接参数。3、pkg-config为不同C/C++库提供编译和链接参数是一项非常繁琐的工作，因此cgo提供了对应pkg-config工具的支持。 我们可以通过#cgo pkg-config xxx命令来生成xxx库需要的编译和链接参数，其底层通过调用 pkg-config xxx --cflags生成编译参数，通过pkg-config xxx --libs命令生成链接参数。 需要注意的是pkg-config工具生成的编译和链接参数是C/C++公用的，无法做更细的区分。pkg-config工具虽然方便，但是有很多非标准的C/C++库并没有实现对其支持。这时候我们可以手工为pkg-config工具创建对应库的编译和链接参数实现支持。比如有一个名为xxx的C/C++库，我们可以手工创建/usr/local/lib/pkgconfig/xxx.bc文件：Name: xxx Cflags:-I/usr/local/include Libs:-L/usr/local/lib –lxxx2 其中Name是库的名字，Cflags和Libs行分别对应xxx使用库需要的编译和链接参数。如果bc文件在其它目录，可以通过PKG_CONFIG_PATH环境变量指定pkg-config工具的检索目录。而对应cgo来说，我们甚至可以通过PKG_CONFIG 环境变量可指定自定义的pkg-config程序。 如果是自己实现CGO专用的pkg-config程序，只要处理--cflags和--libs两个参数即可。下面的程序是macos系统下生成Python3的编译和链接参数：// py3-config.go func main() { for _, s := range os.Args { if s == "--cflags" { out, _ := exec.Command("python3-config", "--cflags").CombinedOutput() out = bytes.Replace(out, []byte("-arch"), []byte{}, -1) out = bytes.Replace(out, []byte("i386"), []byte{}, -1) out = bytes.Replace(out, []byte("x86_64"), []byte{}, -1) fmt.Print(string(out)) return } if s == "--libs" { out, _ := exec.Command("python3-config", "--ldflags").CombinedOutput() fmt.Print(string(out)) return } } } 然后通过以下命令构建并使用自定义的pkg-config工具：$ go build -o py3-config py3-config.go $ PKG_CONFIG=./py3-config go build -buildmode=c-shared -o gopkg.so main.go 4、go get链在使用go get获取Go语言包的同时会获取包依赖的包。比如A包依赖B包，B包依赖C包，C包依赖D包：pkgA -> pkgB -> pkgC -> pkgD -> ...。再go get获取A包之后会依次线获取BCD包。如果在获取B包之后构建失败，那么将导致链条的断裂，从而导致A包的构建失败。链条断裂的原因有很多，其中常见的原因有：不支持某些系统, 编译失败依赖 cgo, 用户没有安装gcc依赖 cgo, 但是依赖的库没有安装依赖 pkg-config, windows上没有安装依赖 pkg-config, 没有找到对应的bc文件依赖 自定义的pkg-config,需要额外的配置依赖 swig,用户没有安装swig,或版本不对仔细分析可以发现，失败的原因中和CGO相关的问题占了绝大多数。这并不是偶然现象，自动化构建C/C++代码一直是一个世界难题，到目前位置也没有出现一个大家认可的统一的C/C++管理工具。因为用了cgo，比如gcc等构建工具是必须安装的，同时尽量要做到对主流系统的支持。如果依赖的C/C++包比较小并且有源代码的前提下，可以优先选择从代码构建。比如github.com/chai2010/webp包通过为每个C/C++源文件在当前包建立关键文件实现零配置依赖：// z_libwebp_src_dec_alpha.c #include "./internal/libwebp/src/dec/alpha.c"因此在编译z_libwebp_src_dec_alpha.c文件时，会编译libweb原生的代码。其中的依赖是相对目录，对于不同的平台支持可以保持最大的一致性。5、多个非main包中导出C函数官方文档说明导出的Go函数要放main包，但是真实情况是其它包的Go导出函数也是有效的。因为导出后的Go函数就可以当作C函数使用，所以必须有效。但是不同包导出的Go函数将在同一个全局的名字空间，因此需要小心避免重名的问题。如果是从不同的包导出Go函数到C语言空间，那么cgo自动生成的_cgo_export.h文件将无法包含全部到处的函数声明，我们必须通过手写头文件的方式什么导出的全部函数。
超文本传输协议（Hypertext Transfer Protocol、HTTP协议）是今天使用最广泛的应用层协议，1989年由Tim Berners-Lee在CERN起草的协议已经成为了互联网的数据传输的核心。在过去几年的时间里，HTTP/2和HTTP/3也对现有的协议进行了更新，提供更加安全和快速的传输功能。多数的编程语言都会在标准库中实现HTTP/1.1和HTTP/2.0已满足工程师的日常开发需求，Go语言的网络库也实现了这两个大版本的HTTP协议。一、设计原理HTTP协议是应用层协议，在通常情况下我们都会使用TCP作为底层的传输层协议传输数据包，但是HTTP/3在UDP协议上实现了新的传输层协议QUIC来传输数据，这也意味着HTTP既可以跑在TCP上，也可以跑在UDP上。Go语言标准库通过net/http包提供HTTP的客户端和服务端实现。请求和响应HTTP协议中最常见的概念是HTTP请求与响应，我们可以将它们理解成客户端和服务端之间传递的消息，客户端向服务端发送HTTP 请求，服务端收到HTTP请求后会做出计算后以HTTP响应的形式发送给客户端。与其他的二进制协议不同，作为文本传输协议，HTTP协议的协议头都是文本数据，HTTP请求头的首行会包含请求的方法、路径和协议版本，接下来是多个HTTP协议头以及携带的负载。GET / HTTP/1.1 User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) Host: draveness.me Accept-Language: en-us Accept-Encoding: gzip, deflate Content-Length: <length> Connection: Keep-Alive ... HTTP响应也有着比较类似的结构，其中也包含响应的协议版本、状态码、响应头以及负载。消息边界HTTP协议目前主要还是跑在TCP协议上的，TCP协议是面向连接的、可靠的、基于字节流的传输层通信协议，应用层交给TCP协议的数据并不会以消息为单位向目的主机传输，这些数据在某些情况下会被组合成一个数据段发送给目标的主机。因为TCP协议是基于字节流的，所以基于TCP协议的应用层协议都需要自己划分消息的边界。在应用层协议中，最常见的两种解决方案就是基于长度或者基于终结符（Delimiter）。HTTP 协议其实同时实现了上述两种方案，在多数情况下HTTP协议都会在协议头中加入Content-Length表示负载的长度，消息的接收者解析到该协议头之后就可以确定当前 HTTP请求/响应结束的位置，分离不同的HTTP消息，下面就是一个使用Content-Length划分消息边界的例子：HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 138 ... Connection: close An Example Page Hello World, this is a very simple HTML document. 不过HTTP协议除了使用基于长度的方式实现边界，也会使用基于终结符的策略，当HTTP使用块传输（Chunked Transfer）机制时，HTTP头中就不再包含Content-Length了，它会使用负载大小为0的HTTP消息作为终结符表示消息的边界。层级结构Go语言的net/http中同时包好了HTTP客户端和服务端的实现，为了支持更好的扩展性，它引入了net/http.RoundTripper和 net/http.Handler两个接口。net/http.RoundTripper是用来表示执行HTTP请求的接口，调用方将请求作为参数可以获取请求对应的响应，而net/http.Handler主要用于HTTP服务器响应客户端的请求：type RoundTripper interface { RoundTrip(*Request) (*Response, error) } HTTP请求的接收方可以实现net/http.Handler接口，其中实现了处理HTTP请求的逻辑，处理的过程中会调用net/http.ResponseWriter接口的方法构造HTTP响应，它提供的三个接口Header、Write和WriteHeader分别会获取HTTP响应、将数据写入负载以及写入响应头：type Handler interface { ServeHTTP(ResponseWriter, *Request) } type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int) } 客户端和服务端面对的都是双向的HTTP请求与响应，客户端构建请求并等待响应，服务端处理请求并返回响应。HTTP请求和响应在标准库中不止有一种实现，它们都包含了层级结构。每个net/http.RoundTripper接口的实现都包含了一种向远程发出请求的过程；标准库中也提供了多种net/http.Handler的实现为客户端的HTTP请求提供不同的服务。二、客户端客户端可以直接通过net/http.Get函数使用默认的客户端net/http.DefaultClient发起HTTP请求，也可以自己构建新的net/http.Client实现自定义的HTTP事务，在多数情况下使用默认的客户端都能满足我们的需求，不过需要注意的是使用默认客户端发出的请求没有超时时间，所以在某些场景下会一直等待下去；除了自定义HTTP事务之外，我们还可以实现自定义的net/http.CookieJar接口来管理和使用HTTP请求中的Cookie，事务和Cookie是我们在HTTP客户端包为我们提供的两个最重要模块。当我们调用net/http.Client.Get方法发出HTTP时，会按照如下的步骤执行：调用net/http.NewRequest根据方法名、URL和请求体构建请求；调用net/http.Transport.RoundTrip开启HTTP事务、获取连接并发送请求；在HTTP持久连接的 net/http.persistConn.writeLoop方法中等待响应；HTTP的客户端中包含几个比较重要的结构体，它们分别是net/http.Client、net/http.Transport和net/http.persistConn：net/http.Client是HTTP客户端，它的默认值是使用net/http.DefaultTransport的HTTP客户端；net/http.Transport是net/http.RoundTripper接口的实现，它的主要作用就是支持 HTTP/HTTPS 请求和HTTP代理；net/http.persistConn封装了一个TCP的持久连接，是我们与远程交换消息的句柄（Handle）；客户端net/http.Client是级别较高的抽象，它提供了HTTP的一些细节，包括Cookies和重定向；而net/http.Transport结构体会处理HTTP/HTTPS协议的底层实现细节，其中会包含连接重用、构建请求以及发送请求等功能。构建请求net/http.Request结构体表示HTTP服务接收到的请求或者HTTP客户端发出的请求，其中包含HTTP请求的方法、URL、协议版本、协议头以及请求体等字段，除了这些字段之外，它还会持有一个指向HTTP响应的引用：type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header Body io.ReadCloser ... Response *Response } net/http.NewRequest是标准库提供的用于创建请求的方法，这个方法会对HTTP请求的字段进行校验并根据输入的参数拼装成新的请求结构体。func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) { if method == "" { method = "GET" } if !validMethod(method) { return nil, fmt.Errorf("net/http: invalid method %q", method) } u, err := urlpkg.Parse(url) if err != nil { return nil, err } rc, ok := body.(io.ReadCloser) if !ok && body != nil { rc = ioutil.NopCloser(body) } u.Host = removeEmptyPort(u.Host) req := &Request{ ctx: ctx, Method: method, URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(Header), Body: rc, Host: u.Host, } if body != nil { ... } return req, nil } 请求拼装的过程比较简单，它会检查对输入的方法、URL以及负载，对它们进行校验并初始化了新的net/http.Request结构体，处理负载的过程稍微有一些复杂，我们会根据负载的类型不同，使用不同的方法将它们包装成io.ReadCloser类型。开启事务当我们使用标准库构建了HTTP请求之后，就会开启HTTP事务发送HTTP请求并等待远程的响应，经过下面一连串的函数调用，我们最终来到了标准库实现底层HTTP协议的结构体 — net/http.Transport：net/http.Client.Donet/http.Client.donet/http.Client.sendnet/http.sendnet/http.Transport.RoundTripnet/http.Transport实现了net/http.RoundTripper接口，也是整个请求过程中最重要并且最复杂的结构体，该结构体会在net/http.Transport.roundTrip方法中发送HTTP请求并等待响应，我们可以将该函数的执行过程分成两个部分：根据 URL 的协议查找并执行自定义的net/http.RoundTripper实现；从连接池中获取或者初始化新的持久连接并调用连接的net/http.persistConn.roundTrip方法发出请求；可以在标准库的net/http.Transport中调用net/http.Transport.RegisterProtocol方法为不同的协议注册net/http.RoundTripper的实现，在下面的这段代码中就会根据URL中的协议选择对应的实现来替代默认的逻辑：func (t Transport) roundTrip(req Request) (*Response, error) { ctx := req.Context() scheme := req.URL.Scheme if altRT := t.alternateRoundTripper(req); altRT != nil { if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol { return resp, err } } ...}在默认情况下我们都会使用net/http.persistConn持久连接处理HTTP请求，该方法会先获取用于发送请求的连接，随后调用net/http.persistConn.roundTrip方法：func (t *Transport) roundTrip(req *Request) (*Response, error) { ... for { select { case <-ctx.Done(): return nil, ctx.Err() default: } treq := &transportRequest{Request: req, trace: trace} cm, err := t.connectMethodForRequest(treq) if err != nil { return nil, err } pconn, err := t.getConn(treq, cm) if err != nil { return nil, err } resp, err := pconn.roundTrip(treq) if err == nil { return resp, nil } } } net/http.Transport.getConn是获取连接的方法，该方法会通过两种方法获取用于发送请求的连接：func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) { req := treq.Request ctx := req.Context() w := &wantConn{ cm: cm, key: cm.key(), ctx: ctx, ready: make(chan struct{}, 1), } if delivered := t.queueForIdleConn(w); delivered { return w.pc, nil } t.queueForDial(w) select { case <-w.ready: ... return w.pc, w.err ... } } 调用net/http.Transport.queueForIdleConn在队列中等待闲置的连接；调用net/http.Transport.queueForDial在队列中等待建立新的连接；连接是一种相对比较昂贵的资源，如果在每次发出HTTP请求之前都建立新的连接，可能会消耗比较多的时间，带来较大的额外开销，通过连接池对资源进行分配和复用可以有效地提高HTTP请求的整体性能，多数的网络库客户端都会采取类似的策略来复用资源。当我们调用net/http.Transport.queueForDial方法尝试与远程建立连接时，标准库会在内部启动新的Goroutine执行net/http.Transport.dialConnFor用于建连，从最终调用的net/http.Transport.dialConn方法中我们能找到TCP连接和net库的身影：func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) { pconn = &persistConn{ t: t, cacheKey: cm.key(), reqch: make(chan requestAndChan, 1), writech: make(chan writeRequest, 1), closech: make(chan struct{}), writeErrCh: make(chan error, 1), writeLoopDone: make(chan struct{}), } conn, err := t.dial(ctx, "tcp", cm.addr()) if err != nil { return nil, err } pconn.conn = conn pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize()) pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize()) go pconn.readLoop() go pconn.writeLoop() return pconn, nil } 在创建新的TCP连接后，我们还会在后台为当前的连接创建两个Goroutine，分别从TCP连接中读取数据或者向TCP连接写入数据，从建立连接的过程我们就可以发现，如果我们为每一个HTTP请求都创建新的连接并启动Goroutine处理读写数据，会占用很多的资源。等待请求持久的TCP连接会实现net/http.persistConn.roundTrip方法处理写入HTTP请求并在select语句中等待响应的返回：func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { writeErrCh := make(chan error, 1) pc.writech <- writeRequest{req, writeErrCh, continueCh} resc := make(chan responseAndError) pc.reqch <- requestAndChan{ req: req.Request, ch: resc, } for { select { case re := <-resc: if re.err != nil { return nil, pc.mapRoundTripError(req, startBytesWritten, re.err) } return re.res, nil ... } } } 每个HTTP请求都是由另一个Goroutine中的net/http.persistConn.writeLoop循环写入的，这两个Goroutine独立执行并通过Channel进行通信。net/http.Request.write方法会根据 net/http.Request结构体中的字段按照HTTP协议组成TCP数据段：func (pc *persistConn) writeLoop() { defer close(pc.writeLoopDone) for { select { case wr := <-pc.writech: startBytesWritten := pc.nwrite wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh)) ... case <-pc.closech: return } } } 当我们调用net/http.Request.write方法向请求中写入数据时，实际上就直接写入了net/http.persistConnWriter中的TCP连接中，TCP协议栈会负责将HTTP请求中的内容发送到目标服务器上：type persistConnWriter struct { pc *persistConn } func (w persistConnWriter) Write(p []byte) (n int, err error) { n, err = w.pc.conn.Write(p) w.pc.nwrite += int64(n) return } 持久连接中的另一个读循环net/http.persistConn.readLoop会负责从TCP连接中读取数据并将数据发送会HTTP请求的调用方，真正负责解析HTTP协议的还是net/http.ReadResponse方法：func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) { tp := textproto.NewReader(r) resp := &Response{ Request: req, } line, _ := tp.ReadLine() if i := strings.IndexByte(line, ' '); i == -1 { return nil, badStringError("malformed HTTP response", line) } else { resp.Proto = line[:i] resp.Status = strings.TrimLeft(line[i+1:], " ") } statusCode := resp.Status if i := strings.IndexByte(resp.Status, ' '); i != -1 { statusCode = resp.Status[:i] } resp.StatusCode, err = strconv.Atoi(statusCode) resp.ProtoMajor, resp.ProtoMinor, _ = ParseHTTPVersion(resp.Proto) mimeHeader, _ := tp.ReadMIMEHeader() resp.Header = Header(mimeHeader) readTransfer(resp, r) return resp, nil } 我们在上述方法中可以看到HTTP响应结构的大致框架，其中包含状态码、协议版本、请求头等内容，响应体还是在读取循环net/http.persistConn.readLoop中根据HTTP协议头进行解析的。三、服务器Go语言标准库net/http包提供了非常易用的接口，如下所示，我们可以利用标准库提供的功能快速搭建新的HTTP服务：func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) } func main() { http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe(":8080", nil)) } 上述的main函数只调用了两个标准库提供的函数，它们分别是用于注册处理器的net/http.HandleFunc函数和用于监听和处理器请求的net/http.ListenAndServe，多数的服务器框架都会包含这两类接口，分别负责注册处理器和处理外部请求，这一种非常常见的模式，我们在这里也会按照这两个维度介绍标准库如何支持HTTP服务器的实现。注册处理器HTTP服务是由一组实现了net/http.Handler接口的处理器组成的，处理HTTP请求时会根据请求的路由选择合适的处理器。当我们直接调用net/http.HandleFunc注册处理器时，标准库会使用默认的HTTP服务器net/http.DefaultServeMux处理请求，该方法会直接调用net/http.ServeMux.HandleFunc：func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { mux.Handle(pattern, HandlerFunc(handler)) } 上述方法会将处理器转换成net/http.Handler接口类型调用net/http.ServeMux.Handle注册处理器：func (mux *ServeMux) Handle(pattern string, handler Handler) { if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } if pattern[0] != '/' { mux.hosts = true } } 路由和对应的处理器会被组成net/http.DefaultServeMux会持有一个net/http.muxEntry哈希，其中存储了从URL到处理器的映射关系，HTTP服务器在处理请求时就会使用该哈希查找处理器。处理请求标准库提供的net/http.ListenAndServe函数可以用来监听 TCP 连接并处理请求，该函数会使用传入的监听地址和处理器初始化一个HTTP服务器net/http.Server，调用该服务器的net/http.Server.ListenAndServe方法：func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } net/http.Server.ListenAndServe方法会使用网络库提供的net.Listen函数监听对应地址上的TCP连接并通过net/http.Server.Serve处理客户端的请求：func (srv *Server) ListenAndServe() error { if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) } net/http.Server.Serve会在循环中监听外部的TCP连接并为每个连接调用net/http.Server.newConn创建新的结构体 net/http.conn，它是HTTP连接的服务端表示：func (srv *Server) Serve(l net.Listener) error { l = &onceCloseListener{Listener: l} defer l.Close() baseCtx := context.Background() ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, err := l.Accept() if err != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } ... return err } connCtx := ctx c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(connCtx) } } 创建了服务端的连接之后，标准库中的实现会为每个HTTP请求创建单独的Goroutine并在其中调用net/http.Conn.serve方法，如果当前HTTP服务接收到了海量的请求，会在内部创建大量的Goroutine，这可能会使整个服务质量明显降低无法处理请求。func (c *conn) serve(ctx context.Context) { c.remoteAddr = c.rwc.RemoteAddr().String() ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) ctx, cancelCtx := context.WithCancel(ctx) c.cancelCtx = cancelCtx defer cancelCtx() c.r = &connReader{conn: c} c.bufr = newBufioReader(c.r) c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) for { w, _ := c.readRequest(ctx) serverHandler{c.server}.ServeHTTP(w, w.req) w.finishRequest() ... } } 上述代码片段是我们简化后的连接处理过程，其中包含读取HTTP请求、调用Handler处理HTTP请求以及调用完成该请求。读取HTTP请求会调用net/http.Conn.readRequest方法，该方法会从连接中获取HTTP请求并构建一个实现了net/http.ResponseWriter接口的变量net/http.response，向该结构体写入的数据都会被转发到它持有的缓冲区中：func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) { ... w.written += int64(lenData) if w.contentLength != -1 && w.written > w.contentLength { return 0, ErrContentLength } if dataB != nil { return w.w.Write(dataB) } else { return w.w.WriteString(dataS) } } 解析了HTTP请求并初始化net/http.ResponseWriter之后，我们就可以调用net/http.serverHandler.ServeHTTP方法查找处理器来处理HTTP请求了：type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) } 如果当前的HTTP服务器中不包含任何处理器，我们会使用默认的net/http.DefaultServeMux处理外部的HTTP请求。net/http.ServeMux是一个HTTP请求的多路复用器，它可以接收外部的HTTP请求、根据请求的URL匹配并调用最合适的处理器：func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { h, _ := mux.Handler(r) h.ServeHTTP(w, r) } 经过一系列的函数调用，上述过程最终会调用HTTP服务器的net/http.ServerMux.match方法，该方法会遍历前面注册过的路由表并根据特定规则进行匹配：func (mux *ServeMux) match(path string) (h Handler, pattern string) { v, ok := mux.m[path] if ok { return v.h, v.pattern } for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" } 如果请求的路径和路由中的表项匹配成功，我们会调用表项中对应的处理器，处理器中包含的业务逻辑会通过net/http.ResponseWriter构建HTTP请求对应的响应并通过TCP连接发送回客户端。Go语言的HTTP标准库提供了非常丰富的功能，很多语言的标准库只提供了最基本的功能，实现HTTP客户端和服务器往往都需要借助其他开源的框架，但是Go语言的很多项目都会直接使用标准库实现HTTP服务器，这也从侧面说明了Go语言标准库的价值。
以太坊的最大优势之一是其公共账本内交易记录的不可篡改性，这些交易包括Token的转移，合约的部署以及合约交易。以太坊网络上的任何节点都可以验证每笔交易的有效性和状态，从而使以太坊成为一个非常强大的去中心化系统。但最大的缺点是，智能合约一旦部署后，则无法更改合约源码。中心化应用程序的开发人员会经常对程序进行更新，修复bug或引入新功能。而这种方式在以太坊上是不可能做到的。著名的Parity Wallet事件，黑客盗取了150000个ETH，在这次的攻击中，Parity multisig钱包中一个合约的漏洞被黑客利用，盗取了钱包中的资金。在黑客攻击过程中，我们唯一能做的就是利用相同的漏洞，比黑客更快速的将钱包中的资金进行转移，并在事后归还给所有者。如果有一种方法可以在智能合约部署后，更新源代码……一、代理模式虽然无法更新已部署的智能合约代码，但是可以通过设置一个代理合约架构，进而部署新的合约，以实现合约升级的目的。代理模式使得所有消息调用都通过代理合约，代理合约会将调用请求重定向到最新部署的合约中。如要升级时，将升级后新合约地址更新到代理合约中即可。代理底层由delegatecall和fallback函数来实现：当调用的方法在合约中不存在时，合约会调用fallback函数。可以编写fallback函数的逻辑处理这种情况。代理合约使用自定义的fallback函数将调用请求重定向到逻辑合约中。每当合约A通过delegatecall到另一个合约B时，它都会在合约A的上下文中执行合约B的代码。这意味着将保留msg.value和msg.sender值，并且每次存储修改的是合约A。代理的核心实现代码（使用强大的汇编代码来解析参数和提供返回值）：pragma solidity ^0.6.6; abstract contract Proxy { function implementation() public virtual view returns (address); fallback() external { address _impl = implementation(); require(_impl != address(0)); assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } } 为了将调用请求代理到另一个合约中，我们必须将代理合约收到的msg.data传递给逻辑合约。由于msg.data的类型为bytes，大小是不固定的，数据大小存储在msg.data的第一个字长（32个字节）中。如果我们只想提取实际数据，则需要跳过前32字节，从 msg.data的0x20（32个字节）位置开始。这里，我们将利用两个操作码来执行该操作。使用calldatasize获得msg.data的大小，使用calldatacopy将其复制到ptr变量中。注意初始化ptr变量。在Solidity中，内存插槽0x40位置是比较特殊的，它包含了下一个可用的空闲内存指针的值。每次将变量直接保存到内存时，都应通过查询0x40位置的值，来确定变量保存在内存的位置。let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) 接下来看一下汇编模块中delegatecall操作码：let result := delegatecall(gas(), target, ptr, calldatasize(), 0, 0) 参数：gas() 传递执行合约所需要燃料target 所请求的目标合约地址ptr 请求数据在内存中的起始位置calldatasize() 请求数据的大小。0 用于表示目标合约的返回值。这是未使用的，因为此时我们尚不知道返回数据的大小，因此无法将其分配给变量。之后我们可以使用returndata操作码访问此信息0 表示目标合约返回值的大小。这是未使用的，因为在调用目标合约之前，我们是无法知道返回值的大小。之后我们可以通过returndatasize操作码来获得该值下一行，使用returndatasize操作码获取返回值的大小let size := returndatasize() 然后，我们使用returndatacopy操作码将返回的数据拷贝到ptr变量中。returndatacopy(ptr, 0, size) 最后，switch语句返回的数据或者抛出异常。因为使用了delegatecall，所以需要注意合约的存储分配。由于我们将一个合约用于存储，而将另一个合约用于逻辑处理，因此任何一个合约都可能覆盖已使用的存储插槽。这意味着，如果代理合约具有状态变量以跟踪某个存储插槽中的最新逻辑合约地址，而该逻辑合约不知道该变量，则该逻辑合约可能会在同一插槽中存储一些其他数据，从而覆盖代理的关键信息。可以使用以下三种代理模式来实现合约的可升级：继承存储永久存储非结构化存储二、继承存储继承存储方式需要逻辑合约包含代理合约所需的存储结构。代理和逻辑合约都继承相同的存储结构，以确保两者都存储必要的代理状态变量。对于这种方式，我们使用Registry合约来跟踪逻辑合同的不同版本。为了升级到新的逻辑合同，开发者需要在注册合约中将新升级的合约进行注册，并要求代理升级到新合约。 interface IRegistry { event ProxyCreated(address proxy); event VersionAdded(string version, address implementation); function addVersion(string calldata version, address implementation) external; function getVersion(string calldata version) external view returns (address); } contract Registry is IRegistry { mapping (string => address) internal versions; function addVersion(string memory version, address implementation) public override { require(versions[version] == address(0)); versions[version] = implementation; emit VersionAdded(version, implementation); } function getVersion(string memory version) public override view returns (address) { return versions[version]; } function createProxy(string memory version) public payable returns (UpgradeabilityProxy) { UpgradeabilityProxy proxy = new UpgradeabilityProxy(version); Upgradeable(address(proxy)).initialize{value:msg.value}(msg.sender); ProxyCreated(address(proxy)); return proxy; } } contract UpgradeabilityStorage { IRegistry internal registry; address internal _implementation; } contract UpgradeabilityProxy is UpgradeabilityStorage,Proxy { constructor(string memory _version) public { registry = IRegistry(msg.sender); upgradeTo(_version); } function implementation() public override view returns (address) { return _implementation; } function upgradeTo(string memory _version) public { _implementation = registry.getVersion(_version); } } abstract contract Upgradeable is UpgradeabilityStorage { function initialize(address sender) public virtual payable; } 如何初始化部署Registry合约部署初始版本目标合约（v1），确保它继承了Upgradeable合约将初始版本目标合约的地址注册到Registry合约调用Registry合约创建一个UpgradeabilityProxy代理合约实例如何升级部署从初始版本继承的新版本合约（v2），并确保新版本合约保留初始版本合约的存储结构。将新版本的合约注册到Registry调用UpgradeabilityProxy，将目标合约升级为新版本。重要要点数据实际是保存在UpgradeabilityProxy中的通过升级合约，可以引入新的方法或状态变量。三、永久存储在永久存储模式中，存储结构是在单独的合约中定义，代理合约和逻辑合约都继承存储合约。存储合约包含逻辑合约所需的所有状态变量，同时，代理合约也能够识别这些状态变量，因此代理合约在定义升级所需要的状态变量时，不必担心所定义的状态变量会被覆盖。但是逻辑合约的后续版本均不应定义任何其他状态变量，逻辑合约的所有版本都必须始终使用最开始定义存储结构。只有代理所有者有权将新版本合约写入代理合约中，或者将所有权进行移交。 contract EternalStorage { mapping(bytes32 => uint256) uintStorage; mapping(bytes32 => string) stringStorage; mapping(bytes32 => address) addressStorage; mapping(bytes32 => bytes) bytesStorage; mapping(bytes32 => bool) boolStorage; mapping(bytes32 => int256) intStorage; } contract UpgradeabilityStorage { string _version; address _implementation; address _upgradeabilityOwner; } contract UpgradeabilityProxy is Proxy, UpgradeabilityStorage { event Upgraded(string version, address indexed implementation); event ProxyOwnershipTransferred(address previousOwner, address newOwner); function version() public view returns (string memory) { return _version; } function implementation() public override view returns (address) { return _implementation; } function upgradeabilityOwner() public view returns (address) { return _upgradeabilityOwner; } function setUpgradeabilityOwner(address newUpgradeabilityOwner) public { _upgradeabilityOwner = newUpgradeabilityOwner; } function _upgradeTo(string memory newVersion, address newImplementation) internal { require(_implementation != newImplementation); _version = newVersion; _implementation = newImplementation; emit Upgraded(newVersion, newImplementation); } } contract OwnedUpgradeabilityProxy is UpgradeabilityProxy { constructor() public { setUpgradeabilityOwner(msg.sender); } modifier onlyProxyOwner() { require(msg.sender == proxyOwner()); _; } function proxyOwner() public view returns (address) { return upgradeabilityOwner(); } function transferProxyOwnership(address newOwner) public onlyProxyOwner { require(newOwner != address(0)); ProxyOwnershipTransferred(proxyOwner(), newOwner); setUpgradeabilityOwner(newOwner); } function upgradeTo(string memory version, address implementation) public onlyProxyOwner { _upgradeTo(version, implementation); } } contract EternalStorageProxy is EternalStorage, OwnedUpgradeabilityProxy {} contract V1 is EternalStorage { function balanceOf(address owner) public view returns (uint256) { return uintStorage[keccak256(abi.encodePacked("balance", owner))]; } function setBalance(address owner, uint256 balance) public { uintStorage[keccak256(abi.encodePacked(owner))] = balance; } } 如何初始化部署EternalStorageProxy合约部署初始版本目标合约（v1）,继承自EternalStorage合约调用EternalStorageProxy合约，将初始版本的目标合约地址注册到代理合约中如果逻辑合约依赖构造函数来设置一些初始状态，则在注册到代理合约之后必须重新初始化，这是因为代理的存储不知道这些值。如何升级部署（v2）版本的目标合约，确保其拥有相同的存储结构。调用EternalStorageProxy，将合约升级到新版本。重要要点新版本合约可以升级现有合约的方法或引入新的方法，但是不能引入新的状态变量。四、非结构化存储非结构化存储模式类似继承存储模式，但并不需要目标合约继承与升级相关的任何状态变量。此模式使用代理合约中定义的非结构化存储插槽来保存升级所需的数据。在代理合约中，我们定义了一个常量变量，在对它进行Hash时，应提供足够随机的存储位置来存储代理合约调用逻辑合约的地址。bytes32 private constant implementationPosition = keccak256("proxy.implementation"); 由于常量不会占用存储插槽，因此不必担心implementationPosition被目标合约意外覆盖。由于Solidity状态变量存储的规定，目标合约中定义的其他内容使用此存储插槽冲突的可能性极小。通过这种模式，逻辑合约不需要知道代理合约的存储结构，但是所有未来的逻辑合约都必须继承其初始版本定义的存储变量。就像在继承存储模式中一样，将来升级的目标合约可以升级现有功能以及引入新功能和新存储变量。只有代理所有者有权将新版本合约写入代理合约中，或者将所有权进行移交。 contract UpgradeabilityProxy is Proxy { event Upgraded(address indexed implementation); bytes32 private constant implementationPosition = keccak256("proxy.implementation"); function implementation() public override view returns (address impl) { bytes32 position = implementationPosition; assembly { impl := sload(position) } } function setImplementation(address newImplementation) internal { bytes32 position = implementationPosition; assembly { sstore(position, newImplementation) } } function _upgradeTo(address newImplementation) internal { address currentImplementation = implementation(); require(currentImplementation != newImplementation); setImplementation(newImplementation); emit Upgraded(newImplementation); } } contract OwnedUpgradeabilityProxy is UpgradeabilityProxy { event ProxyOwnershipTransferred(address previousOwner, address newOwner); bytes32 private constant proxyOwnerPosition = keccak256("proxy.owner"); constructor() public { setUpgradeabilityOwner(msg.sender); } modifier onlyProxyOwner() { require(msg.sender == proxyOwner()); _; } function proxyOwner() public view returns (address owner) { bytes32 position = proxyOwnerPosition; assembly { owner := sload(position) } } function setUpgradeabilityOwner(address newProxyOwner) internal { bytes32 position = proxyOwnerPosition; assembly { sstore(position, newProxyOwner) } } function transferProxyOwnership(address newOwner) public onlyProxyOwner { require(newOwner != address(0)); emit ProxyOwnershipTransferred(proxyOwner(), newOwner); setUpgradeabilityOwner(newOwner); } function upgradeTo(address implementation) public onlyProxyOwner { _upgradeTo(implementation); } } contract V1 { mapping (address => uint256) internal _balances; function balanceOf(address owner) public view returns (uint256) { return _balances[owner]; } function setBalance(address owner, uint256 balance) public { _balances[owner] = balance; } } 如何初始化部署OwnedUpgradeabilityProxy合约部署初始版本（v1）的目标合约调用OwnedUpgradeabilityProxy合约将初始版本的目标合约注册到代理合约中如果您的逻辑合约依赖于其构造函数来设置一些初始状态，则在注册到代理之后必须重做，因为代理的存储不知道这些值。如何升级部署（v2）版本的合约，确保它继承了先前版本中使用的状态变量。调用OwnedUpgradeabilityProxy合约，将目标合约升级到新版本。重要要点这种方式最实用，目标合约与代理合约耦合性最低。参考：Proxy Patterns