如何申请交换友情链接交换群

深入Go语言 - 13 - 推酷
深入Go语言 - 13
本章重点介绍Go语言中的反射。
可以实现运行时的反射,允许程序操纵对象的值和类型。
典型地,你可以获取 interface{}的动态类型以及的它的值和方法。
Go是静态类型的语言,每一个对象在声明和初始化的时候都已经有一个确定值,即使是声明为接口类型的变量,它的静态类型也已经确定,即使任何包含这个接口方法集的类型的对象都可以赋值给它。
我们可以在运行时获取对象的动态类型和值。
类型Type和值Value是我们使用发射库的主要用的两个概念。
是一个interface,代表Go中的一个类型,可以把它看成某个类型的元数据(描述类型的类型),这个类型既可以是Go语言或者库中定义的type类型,也可以你自己定义的type类型。
下面我们介绍它的主要方法以及一些辅助方法。
值得注意的是,并不是所有的方法都对某种类型有效,比如有些方法只对函数类型有意义,有的只对Struct有意义。如果对某个类型调用了错误的方法,则会发生运行时的panic,比如针对struct Type,调用
IsVariadic
方法。所以最好先判断一下Type的Kind。
首先我们先为下面的例子定义一个简单的struct用来测试, Bird有三个字段Field和三个方法,其中两个字段和两个方法是导出的:
typeBirdstruct{
Name string
Color string
func(b Bird) Sing()string{
return"sing"
func(b *Bird) Fly()string{
return"fly"
func(b *Bird) food() {
返回一个接口对象的动态类型,也就是类型
reflect.Type
的一个值:
varbird = Bird{Name:"parrot", Color:"blue"}
vart reflect.Type
t = reflect.TypeOf(bird)
返回动态类型的名称,包名以及字符串表示。
fmt.Println(t.Name()) //Bird
fmt.Println(t.String()) //main.Bird
fmt.Println(t.PkgPath()) //main
Name返回类型名,如Bird,对于未定义名称的类型,返回空字符串。
PkgPath返回包名,代表这个包的唯一标识符,所以可能是单一的包名,或者
encoding/base64
。对于Go内置的类型string,error等,或者未定义名称的类型struct{}等,则返回空字符串。
String方法返回这个类型的字符串表示,包名可能用简写表示。
返回类型的分类。 你自己定义的类型必定属于下面的某一类:
Invalid Kind = iota
Complex128
UnsafePointer
fmt.Println(t.Kind()) //struct
FieldAlign
fmt.Println(t.Size()) //40
//fmt.Println(t.Bits()) //panic
fmt.Println(t.Align()) //8
fmt.Println(t.FieldAlign()) //8
返回存储这个类型的一个值所需要的字节数,它返回要保存储这个值需要的内存,而不会计算它引用的内存的大小。 比如字符串是以
StringHeader
类型来存储的,它包含一个指针指向内存字符存储的数据,它的size只计算存储这个结构所需的内存,而不会计算指针指向的数据占用的字节数的,数据会对齐的。
返回类型的size,以bit计算,但是如果类型不是Int、Uint、Float、Complex之一则panic。
是这个类型的一个变量在内存中的对齐后的所用的字节数。
FieldAlign
指这种类型的变量如果是struct中的字段,那么它对齐后所用的字节数。
对于gc编译器来讲,
FieldAlign
是一样的,但是对于gccgo来讲,它们可能不同。
还有下面会讲到的
reflect.StructField.Offset
,它是某个字段在struct中的偏移量,一起考虑了前面字段的对齐。
为什么要对齐?
第一个原因——很多CPU只从对齐的地址开始加载数据,而有的CPU这样做,只是更快一点。
第二个原因——外部总线从内存一次获取的数据往往不是1byte,而是4bytes或许8bytes,或者更多~~
是值占用的字节数,而
是变量占的字节数。
比如在我当前的开发环境下(64-bit windows 10), 字符串的
为16个字节,而字符串的
为8, int类型的
都是8, int32的
Implements
AssignableTo
ConvertibleTo
Comparable
Implements
返回类型是否实现了某个接口,如果参数的类型不是interface类型,则会panic。
varr *io.Reader
t1 := reflect.TypeOf(r).Elem()
t2 := reflect.TypeOf(&os.Stdout).Elem()
fmt.Println(t.Implements(t1)) //false
fmt.Println(t2.Implements(t1)) //true
AssignableTo
一个类型的值是否可以赋值给参数指定的类型,下面的例子中Bird类型的指针对象可以赋值给IBird接口:
typeIBirdinterface{
Sing() string
Fly() string
vari IBird = &bird
fmt.Println(t.AssignableTo(reflect.TypeOf(i).Elem()))
ConvertibleTo
一个类型的值是否可以转换成另一个类型的值:
typeBird2 Bird
varbird2 Bird2
t3 := reflect.TypeOf(bird2)
fmt.Println(t3.ConvertibleTo(t)) //true
bird = Bird(bird2) //可以转换
Comparable
返回类型是否可以比较:
fmt.parable()) //true
类型如果可以比较,就可以使用
运算符。可以参看比较运算符。
FieldByIndex
FieldByName
FieldByNameFunc
这一组方法用来获取Struct的Field的信息,这个信息通过StructField类型来描述。如果类型不是Struct,调用相应的方法导致运行时panic。
返回Struct的字段的数量。
fmt.Println(t4.NumField()) //1
返回struct的第i个字段的信息,包括未导出对象的信息
fmt.Println(t.Field(0))//{Name string 0 [0] false}
fmt.Println(t.Field(1))//{Color string 16 [1] false}
fmt.Println(t.Field(2))//{/smallnest/dive-into-go/ch12/model int 32 [2] false}
FieldByIndex
对于嵌套的Struct,可以递归地得到某一层的字段的信息:
typeS1struct{
Name string
typeS2struct{
typeS3struct{
t4 := reflect.TypeOf(s)
fmt.Println(t4.FieldByIndex([]int{0,0,1}))//{Age int 16 [1] false}
这个例子S3嵌套S2,S2嵌套S1,我们通过0,0,1就可以得到S1的字段Age的信息。
FieldByName
根据名称得到字段的信息:
fmt.Println(t4.FieldByName("Name"))//{Name string 0 [0 0 0] false} true
FieldByNameFunc
根据一个函数筛选字段,返回第一个符合条件的字段:
fmt.Println(t4.FieldByNameFunc(func(nstring)bool{
ifn =="Age"{
returntrue
returnfalse
MethodByName
这是一组操作类型的方法的一组方法。
对于非接口的类型T或者*T,它的方法类型和函数字段描述了方法的第一个参数就是receiver。
对于接口类型,它的方法类型的Func字段总是为nil。
返回这个类型的方法集中方法的数量,
返回第i个方法的信息:
fmt.Println(t.NumMethod()) //1
fmt.Println(t.Method(0))//{Sing func(model.Bird) string &func(model.Bird) string Value& 0}
t5 := reflect.TypeOf(&bird)
fmt.Println(t5.NumMethod()) //3
fmt.Println(t5.Method(0))//{Fly func(*model.Bird) string &func(*model.Bird) string Value& 0}
fmt.Println(t5.Method(1))//{Sing func(*model.Bird) string &func(*model.Bird) string Value& 1}
fmt.Println(t5.Method(2))//{/smallnest/dive-into-go/ch12/model func(*model.Bird) &func(*model.Bird) Value& 2}
MethodByName
则是根据名称查找方法。
IsVariadic
对于函数类型,我们关注的是它的输入参数信息
和输出参数信息
IsVariadic
返回函数是否是变参的。
f := http.ListenAndServe
ft := reflect.TypeOf(f)
fmt.Println(ft.NumIn()) //2
fmt.Println(ft.In(0))//string
fmt.Println(ft.NumOut()) //1
fmt.Println(ft.Out(0))//error
fmt.Println(ft.IsVariadic()) //false
方法返回map类型的key的类型,如果不是map类型,则调用此方法会panic。
varmmap[string]int
mt := reflect.TypeOf(m)
fmt.Println(mt.Key())
方法返回类型 Array, Chan, Map, Ptr, Slice的元素的类型。
对于Map类型,它返回的值的类型。
对于指针类型Ptr,它返回指针指向的元素的类型。
对于Chan,它返回传递的元素的类型。
数组和Slice返回的是它包含的元素的类型。
返回数组的长度,因为数组的类型中包含长度的定义。如果不是数组则会panic。
返回channel的方向, chan、chan&-、&-chan
Value描述对象的值信息,同样,并不是所有的方法对任何的类型都有意义,特定的方法只适用于特定的类型。
零值代表没有值,它的
总是返回false,它的
总是返回Invalid,它的String总是返回&
A Value can be used concurrently by multiple goroutines provided that the underlying Go value can be used concurrently for the equivalent direct operations.
Using == on two Values does not compare the underlying values they represent, but rather the contents of the Value structs. To compare two Values, compare the results of the Interface method.
本文中不准备详细阐述Value对象的方法。不是这些方法不重要,而是它的方法太多了。
它针对每一中类型的操作都提供了相应的方法,比如slice类型,有Append、AppendSlice等方法、Map有MapIndex、MapKeys等方法、Struct有Field、FieldByIndex等方法。 注意Value的Field方法返回的是字段的值类型Value,而不是字段的类型描述Type。还有针对数值型、Bool型、Channel类型的方法等。
返回指针指向的对象,如果不是指针,则返回参数本身。
返回一个可寻址的对象的指针。 只有slice的元素、可寻址的数组的元素、可寻址的struct的字段、指针的可寻址的结果才可以调用Addr方法。可以通过
检查。可以查看Go规范中的描述:
返回类型的零值。
一组SetXxx方法用来设置值的值(好绕口)。
下面是一组辅助函数,生成Value对象:
生成某个类型的值Value。
从接口对象返回Value。
1、 从接口对象到反射对象
varxfloat64=3.4
fmt.Println("type:", reflect.TypeOf(x))
fmt.Println("value:", reflect.ValueOf(x))
2、从反射对象到接口对象
func(v Value) Interface()interface{}
如果想转为特定的类型的对象,可以用type assertion:
y := v.Interface().(float64)
3、修改反射对象对象的值必须是可设置的,可以用CanSet方法判断。
varxfloat64=3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
也有一些第三方的简化go reflect的库,如
反射经常用在序列化和反序列的实现中,如官方的
但是请记住一点,发射的性能并不高,所以很多序列化库采用代码模版的方式生成Model对象,而不是反射的方式序列化和反序列化对象。
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致&&11099 阅读
最近在Go语言的QQ群里看到关于图灵社区有牛人老赵吐槽许式伟《Go语言编程》的各种争论.
我之前也看了老赵吐槽许式伟《Go语言编程》的文章, 当时想老赵如果能将许大书中不足部分补充完善了也是一个好事情. 因此, 对老赵的后续文章甚是期待.
谁知道看了老赵之后的两篇吐槽Go语言的文章, 发现完全不是那回事情, 吐槽内容偏差太远.
本来没想掺和进来, 但是看到QQ群里和图灵社区有很多人甚至把老赵的文章当作真理一样.
实在忍不住, 昨天注册了帐号, 进来也说下我的观点.
这是老赵的几篇文章:
因为当前这篇文章主要是针对老赵的做
评论. 因为标题的原因, 也造成了很大的争议性(因为很多人说我理解的很多观点和老赵的原文不相符).
后面我会对Go语言的一些特性一些简单的介绍, 但是不会是现在这种方式.
所谓Go语言式的接口,就是不用显示声明类型T实现了接口I,只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方。这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing。除了Go的接口以外,类似的东西也有比如Scala里的Traits等等。有人觉得这个特性很好,但我个人并不喜欢这种做法,所以在这里谈谈它的缺点。当然这跟动态语言静态语言的讨论类似,不能简单粗暴的下一个“好”或“不好”的结论。
Go的隐式接口其实就是静态的Duck Typing. 很多语言(主要是动态语言)早就有.
静态类型和动态类型没有绝对的好和不好.
Go的隐式接口Duck Typing确实不是新技术, 但是在主流静态编程语言中支持Duck Typing应该是很少的(不清楚目前是否只有Go语言支持).
静态类型和动态类型虽然没有绝对的好和不好, 但是每个都是有自己的优势的, 没有哪一个可以包办一切. 而Go是试图结合静态类型和动态类型(interface)各自的优势.
那么就从头谈起:什么是接口。其实通俗的讲,接口就是一个协议,规定了一组成员,例如.NET里的ICollection接口:
public interface ICollection {
int Count { }
object SyncRoot { }
bool IsSynchronized { }
void CopyTo(Array array, int index);
这就是一个协议的全部了吗?事实并非如此,其实接口还规定了每个行为的“特征”。打个比方,这个接口的Count除了需要返回集合内元素的数目以外,还隐含了它需要在O(1)时间内返回这个要求。这样一个使用了ICollection接口的方法才能放心地使用Count属性来获取集合大小,才能在知道这些特征的情况下选用正确的算法来编写程序,而不用担心带来性能问题,这才能实现所谓的“面向接口编程”。当然这种“特征”并不但指“性能”上的,例如Count还包含了例如“不修改集合内容”这种看似十分自然的隐藏要求,这都是ICollection协议的一部分。
接口就是一个协议, 规定了一组成员.
接口还规定了每个行为对应时间复杂度的&特征&.
接口还规定了每个行为还包含是否会修改集合的隐藏要求.
第一条: 没什么可解释的, 应该是接口的通俗含义.
第二条: 但是接口还包含时间复杂度的&特征&就比较扯了. 请问这个特征是由语言特性来约束(语言如何约束?), 还只是由接口的文档作补充说明(这是语言的特性吗)?
第三条: 这个还算是吐槽到了点子上. Go的接口确实不支持C++类似的const修饰, 除了接口外的method也不支持(Go的const关键字是另一个语义).
但是, C++中有了const就真的安全了吗?
class Foo {
private: mutable Mutex mutex_;
public: void doSomething()const {
MutexLocker locker(&mutex_);
// const 已经被绕过了
C++中方法const修饰唯一的用处就是增加各种编译麻烦, 对使用者无法作出任何承诺. 使用者更关心的是doSomething的要做什么, 上面的方法其实和void doSomethingConst()要表达的是类似的意思.
不管是静态库还是动态库, 哪个能从库一级保证某个函数是不能干什么的? 如果C++的const关键字并不能
真正的保证const, 而类似的实现细节(也包括前面提到的和时间复杂度相关的性能特征)必须有文档来补充.
那文档应该以什么形式提供(代码注释?Word文档?其他格式文档?)? 这些文档真多能保证每个都会有人看吗?
文档说到底还只是人之间的口头约定, 如果文档真的那么好使(还有实现), 那么汇编语言也可以解决一切问题.
在Go语言是如何解决const和性能问题的?
首先, 对于C语言的函数参数传值的语义, const是必然的结果.
但是, 如果参数太大要考虑性能的话, 就会考虑传指针(还是传值的语义), 通过传指针就不能保证const的语义了. 如果连使用的库函数都不能相信, 那怎么就能相信它对于的头文件所提供的const信息呢?
因为, const和性能是相互矛盾的. Go语言中如果想绝对安全, 那就传值. 如果想要性能(或者是返回副作用),
那就传指针:
type Foo int
func (self *Foo)Get() int {
return *self
func (self Foo)GetConst() int {
return self
Go语言怎么对待性能问题(还有单元测试问题)? 答案是集成go test测试工具. 在Go语言中测试代码是pkg(包含package main)的一个组成部分. 不仅是普通的pkg可以go test, package main也可以用go test进行测试.
我们给前面的代码加上单元测试和性能测试.
// foo_test.go
func TestGet(t *testing.T) {
var foo Foo = 0
if v := foo.Get(); v != 0 {
t.Errorf(&Bad Get. Need=%v, Got=%v&, 0, v)
func TestGetConst(t *testing.T) {
var foo Foo = 0
if v := foo.GetConst(); v != 0 {
t.Errorf(&Bad GetConst. Need=%v, Got=%v&, 0, v)
func BenchmarkGet(b *testing.B) {
var foo Foo = 0
for i := 0; i & b.N; i++ {
_ = foo.Get()
func BenchmarkGetConst(b *testing.B) {
var foo Foo = 0
for i := 0; i & b.N; i++ {
_ = foo.GetConst()
当然, 最终的测试结果还是给人来看的. 如果实现者/使用者故意搞破坏, 再好的工具也是没办法的.
由此我们还可以解释另外一些问题,例如为什么.NET里的List不叫做ArrayList,当然这些都只是我的推测。我的想法是,由于List与IList接口是配套出现的,而像IList的某些方法,例如索引器要求能够快速获取元素,这样使用IList接口的方法才能放心地使用下标进行访问,而满足这种特征的数据结构就基本与数组难以割舍了,于是名字里的Array就显得有些多余。
假如List改名为ArrayList,那么似乎就暗示着IList可以有其他实现,难道是LinkedList吗?事实上,LinkedList根本与IList没有任何关系,因为它的特征和List相差太多,它有的尽是些AddFirst、InsertBefore方法等等。当然,LinkedList与List都是ICollection,所以我们可以放心地使用其中一小部分成员,它们的行为特征是明确的。
推测: 因为为了和IList&T&接口配套出现的原因, 才没有将List&T&命名为ArrayList&T&.
因为IList&T&(这个应该是笔误, 我觉得作者是说List&T&)索引器要求能够快速获取元素, 这样使用IList接口的方法才能放心地使用下标进行访问(实现的算法复杂度特征向接口方向传递了).
不能将List&T&改为ArrayList&T&的另一个原因是LinkedList&T&. 因为List&T&和LinkedList&T&的时间复杂度不一样, 所以不能是一个接口(大概是一个算法复杂度一个接口的意思?).
LinkedList&T&与List&T&都属于ICollection&T&这个祖宗接口.
第一条: 我不知道原作者是怎么推测的. 接口的本意就是要和实现分离. 现在却完全绑定到一起了, 那这样还要接口做什么(一个Xxx&T&对应一个IXxx&T&接口)?
第二条: 因为运行时向接口传递了某个时间复杂度的实现, 就推导出接口的都符合某种时间复杂度, 逻辑上根本就不通!
第三条: 和前两个差不多的意思, 没什么可说的.
第四条: 这个应该是Go非入侵接口的优点. C++/Java就是因为接口的入侵性, 才导致了接口和实现无法完全分离. 因为, C++/Java大部分时间都在整理接口间/实现间的祖宗八代之间的关系了(重要的不是如何分类, 而是能做什么). 可以参考许式伟给的Java的例子(了解祖宗八代之间的关系真的很重要吗): .
这方面的反面案例之一便是Java了。在Java类库中,ArrayList和LinkedList都实现了List接口,它们都有get方法,传入一个下标,返回那个位置的元素,但是这两种实现中前者耗时O(1)后者耗时O(N),两者大相近庭。那么好,我现在要实现一个方法,它要求从第一个元素开始,返回每隔P个位置的元素,我们还能面向List接口编程么?假如我们依赖下标访问,则外部一不小心传入LinkedList的时候,算法的时间复杂度就从期望的O(N/P)变成了O(N2/P)。假如我们选择遍历整个列表,则即便是ArrayList我们也只能得到O(N)的效率。话说回来,Java类库的List接口就是个笑话,连Stack类都实现了List,真不知道当年的设计者是怎么想的。
简单地说,假如接口不能保证行为特征,则“面向接口编程”没有意义。
Java的ArrayList和LinkedList都实现了List接口, 但是get方法的时间复杂度不同.
假如接口不能保证行为特征,则“面向接口编程”没有意义。
第一条: 这其实是原作者列的一个前提, 是为了推出第二条的结论. 但是, 我觉得这里的逻辑同样是有问题的. 有这个例子只能说明接口有它的不足, 但是怎么就证明了 则“面向接口编程”没有意义?
第二条: 我要反问一句, 为什么非要在这里使用接口(难道是被C++/Java的面向对象洗脑了)? 接口有它合适的地方(面向逻辑层面), 也有它不合适的地方(面向底层算法层面). 在这里为什么不直接使用ArrayList或LinkedList?
而Go语言式的接口也有类似的问题,因为Structural Typing都只是从表面(成员名,参数数量和类型等等)去理解一个接口,并不关注接口的规则和含义,也没法检查。忘了是Coursera里哪个课程中提到这么一个例子:
nterface IPainter {
void Draw();
nterface ICowBoy {
void Draw();
在英语中Draw同时具有“画画”和“拔枪”的含义,因此对于画家(Painter)和牛仔(Cow Boy)都可以有Draw这个行为,但是两者的含义截然不同。假如我们实现了一个“小明”类型,他明明只是一个画家,但是我们却让他去跟其他牛仔决斗,这样就等于让他去送死嘛。另一方面,“小王”也可以既是一个“画家”也是个“牛仔”,他两种Draw都会,在C#里面我们就可以把他实现为:
class XiaoWang : IPainter, ICowBoy {
void IPainter.Draw() {
void ICowBoy.Draw() {
因此我也一直不理解Java的取舍标准。你说这样一门强调面向对象强调接口强调设计的语言,还要求强制异常,怎么就不支持接口的显示实现呢?
不同实现的Draw含义不同, 因此接口最好也能支持不同的实现.
Java/Go之类的接口都没有C#的接口强大.
第一条: 不要因为自己有个锤子, 就把什么东西都当作钉子! 你这个是C#的例子(我不懂C#), 但是请不要往Go语言上套! 之前是C++搞出了个函数重载(语义还是相似的, 但是签名不同), 没想到C#还搞了个支持同一个单词不同含义的特性.
第二条: 只能说原作者真的不懂Go语言.
Go语言为什么不支持这些花哨的特性? 因为, 它们太复杂且没多大用处, 写出的代码不好理解(如果原作者不提示, 谁能发现Darw的不同含义这个坑?). Go语言的哲学是: &Less is more!&.
看看Go语言该怎么做:
type Painter interface {
type CowBoyer interface {
DrawTheGun()
type XiaoWang struct {
func (self *XiaoWang)Draw() {
func (self *XiaoWang)DrawTheGun() {
XiaoWang需要关心的只是自己有哪些功能(method), 至于祖宗关系开始根本不用关心.
等到XiaoWang各种特性逐渐成熟稳定之后, 发现新来的XiaoMing也有类似的功能特征,
这个时候才会考虑如何用接口来描述XiaoWang和XiaoMing共同特征.
这就是我更倾向于Java和C#中显式标注异常的原因。因为程序是人写的,完全不会因为一个类只是因为存在某些成员,就会被当做某些接口去使用,一切都是经过“设计”而不是自然发生的。就好像我们在泰国不会因为一个人看上去是美女就把它当做女人,这年头的化妆和PS技术太可怕了。
接口是经过“设计”而不是自然发生的.
接口有不足, 因为在泰国不能根据美女这个接口来推断这个人是女人这个类型.
Go的哲学是先构造具体对象, 然后再根据共性慢慢归纳出接口, 一开始不用关心祖宗八代的关系.
请问女人是怎么定义的, 难道这不是一个接口?
我这里再小人之心一把:我估计有人看到这里会说我只是酸葡萄心理,因为C#中没有这特性所以说它不好。还真不是这样,早在当年我还没听说Structural Typing这学名的时候就考虑过这个问题。我写了一个辅助方法,它可以将任意类型转化为某种接口,例如:
XiaoMing xm = new XiaoMing();
ICowBoy cb = StructuralTyping.From(xm).To&ICowBoy&();
于是,我们就很快乐地将只懂画画的小明送去决斗了。其内部实现原理很简单,只是使用Emit在运行时动态生成一个封装类而已。此外,我还在编译后使用Mono.Cecil分析程序集,检查From与To的泛型参数是否匹配,这样也等于提供了编译期的静态检查。此外,我还支持了协变逆变,还可以让不需要返回值的接口方法兼容存在返回值的方法,这可比简单通过名称和参数类型判断要强大多了。
C#接口的这个特性很NB...
我们看看Go是该怎么写(基于前面的Go代码, 没有Draw重载):
var xm interface{} = new(XiaoWang)
cb := xm.(Painter).(CowBoyer)
但是, 我觉得这样写真的很变态. Go语言是为了解决实际的工程问题的,
不是要像C++那样成为各种NB技术的大杂烩.
我始终认同一个观点: 任何语言都可以写出垃圾代码, 但是不能以这些垃圾代码来证明原语言也垃圾.
有了多种选择,我才放心地说我喜欢哪个。JavaScript中只能用回调编写代码,于是很多人说它是JavaScript的优点,说回调多么多么美妙我会深不以为然——只是没法反抗开始享受罢了嘛……
这篇文章好像吐槽有点多?不过这小文章还挺爽的。
这段不是接口相关, 懒得整理/吐槽了.
最后我只想说一个例子, 从C语言时代就很流行的printf函数.
我们看看Go语言中是什么样子(fmt.Fprintf):
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
在Go语言中, fmt.Fprintf只关心怎么识别各种a ...interface{}, 怎么format这些参数,
至于怎么写, 写到哪里去那完全是w io.Writer的事情.
这里第一个参数的w io.Writer就是一个接口, 它不仅可以写到File, 也可以写到net.Conn, 准确的说是可以写到任何实现了io.Writer接口的对象中.
因为, Go语言接口的非入侵性, 我们可以独立实现自己的对象, 只要符合io.Writer接口就行, 然后就可以和fmt.Fprintf配合工作.
后面的可变参数interface{}同样是一个接口, 它代替了C语言的void*, 用于格式化输出各种类型的值. (更准确的讲, 除了基础类型, 参数a必须是一个实现了Stringer接口的扩展类型).
接口是一个完全正交的特性, 可以将Fprintf从各种a ...interface{}, 以及各种w io.Writer完全剥离出来.
Go语言也是这样, struct等基础类型的内存布局还是和C语言中一样, 只是加了个method(在Go1.1中, method value就是一个普通闭包函数), 接口以及goroutine都是在没有破坏原有的类型语义基础上正交扩展(而不是像C++那样搞个构造函数, 以后又是析构函数的).
我到很想知道, 在C++/C#/Java之类的语言中, 是如何实现fmt.Fprintf的.
套用原作者的一句话作为结束: Go语言虽然有缺点, 即使老赵是牛人, 但是这篇吐槽也着实一般!

我要回帖

更多关于 友情链接交换工具 的文章

 

随机推荐