本文中译于官方FAQ
Go语言中的类型
Go是一种面向对象的语言吗?
不是。尽管Go具有类型和方法,并允许使用面向对象的编程风格,但没有类型层次结构。Go中的“接口”概念提供了一种不同的方法,我们认为该方法易于使用,并且在某些方面更通用。还有一些方法可以将类型嵌入其他类型,以提供与子类类似(但不完全相同)的东西。而且,Go中的方法比C ++或Java中的方法更通用:可以为任何类型的数据定义它们,甚至可以将内置类型(例如普通的“未装箱”整数)定义为它们。它们不限于结构(类)。
而且,缺乏类型层次结构使得Go中的“对象”比C ++或Java等语言更轻量。
如何获得方法的动态分配?
动态分配方法的唯一方法是通过接口。结构体或任何其他具体类型上的方法总是静态解析的。
为什么没有类型继承?
至少以最著名的语言进行的面向对象编程涉及对类型之间的关系的过多讨论,而这些关系通常可以自动派生。Go采用了不同的方法。
无需要求程序员提前声明两种类型相关联,在Go中,一种类型可以自动满足任何指定其方法子集的接口。除了减少标记之外,这种方法还具有真正的优势。类型可以一次满足许多接口,而无需传统的多重继承的复杂性。接口可以非常轻量级-具有一个或甚至零个方法的接口可以表达一个有用的概念。如果有新想法或需要测试,则可以在Interfaces之后添加接口,而无需注释原始类型。因为类型和接口之间没有明确的关系,所以没有类型层次结构可以管理或讨论。
可以使用这些思想来构造类似于类型安全的Unix管道的内容。例如,查看如何fmt.Fprintf 对所有输出(不仅是文件)启用格式化打印,或者如何将 bufio程序包与文件I / O完全分开,或者image程序包如何生成压缩的图像文件。所有这些想法都源于表示单个方法(Write)的单个接口(io.Writer)。这只是表面。 Go的界面对程序的结构产生了深远的影响。
这需要一些时间来适应,但是这种隐式的类型依赖样式是Go最具生产力的事情之一。
为什么len是函数而不是方法?
我们讨论了这个问题,但是决定实现len和朋友在一起,因为函数在实践中很好,并且没有使关于基本类型的接口(从Go类型的意义上)的问题变得复杂。
为什么Go不支持方法和运算符的重载?
如果方法分派也不需要进行类型匹配,则可以简化方法分派。其他语言的经验告诉我们,使用具有相同名称但签名不同的多种方法有时会很有用,但在实践中也可能会造成混淆和脆弱。在Go的类型系统中,仅按名称进行匹配并要求类型一致是简化的主要决定。
关于重载,似乎比绝对要求更方便。同样,没有它,事情会更简单。
为什么Go没有implements声明?
Go类型通过实现该接口的方法来满足该接口,仅此而已。此属性允许定义和使用接口,而无需修改现有代码。它实现了一种 结构化类型化,可以促进关注点分离并提高代码重用性,并使得在代码开发时出现的模式上更容易构建。接口的语义是Go敏捷,轻便的感觉的主要原因之一。
有关更多详细信息,请参见类型继承问题。
如何保证我的类型满足接口要求?
可以要求编译器通过尝试使用T的零值或指向T的指针进行赋值来检查类型T是否实现了接口I:
type T struct{}
var _ I = T{} // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.
如果T(或* T,相应地)没有实现I,则错误将在编译时捕获。
如果希望某个接口的用户明确声明他们实现了该接口,则可以在该接口的方法集中添加一个具有描述性名称的方法。例如:
type Fooer interface {
Foo()
ImplementsFooer()
}
然后,类型必须将ImplementsFooer方法实现为Fooer,清楚地记录事实并在go doc的输出中宣布该事实。
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
大多数代码不使用此类约束,因为它们限制了界面概念的实用性。但是有时候,它们对于解决相似接口之间的歧义是必不可少的。
为什么类型T不满足Equal接口?
考虑这个简单的接口来表示一个对象,该对象可以将自己与另一个值进行比较:
type Equaler interface {
Equal(Equaler) bool
}
和这种类型T:
type T int
func (t T) Equal(u T) bool { return t == u } // 不满足等于
与某些多态类型系统中的类似情况不同,T不实现Equaler。 T.Equal的参数类型是T,而不是字面意义上必需的类型Equaler。
在Go中,类型系统不会提升Equal的论点。这是程序员的责任,如类型T2所示,它实现了Equaler:
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } // 满足等于
但是,即使这与其他类型系统都不一样,因为在Go语言中,任何 满足条件的类型Equaler都可以作为参数传递给T2.Equal,并且在运行时我们必须检查参数是否为type T2。一些语言会在编译时做出保证。
一个相关的例子是相反的:
type Opener interface {
Open() Reader
}
func (t T3) Open() *os.File
在Go中,T3不满足于Opener,尽管它可能是另一种语言。
在这种情况下,Go的类型系统确实对程序员没有多大作用,但是缺少子类型化使得关于接口满意度的规则非常容易陈述:函数的名称和签名是否与接口的名称和签名完全相同?Go的规则也易于有效实施。我们认为这些好处弥补了自动类型提升的不足。如果有一天可以采用某种形式的多态类型输入,我们希望将有一种表达这些示例思想的方法,并且可以对它们进行静态检查。
我可以将[]T转换成 []interface{} 吗?
不能直接转换,语言规范不允许这样做,因为这两种类型在内存中的表示方式不同。有必要将元素分别复制到目标切片。本示例将一个切片转换为int的一个切片 interface{}:
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
如果[]T1和[]T2具有相同的基础类型,我可以将[]T1转换为[]T2吗?
此代码示例的最后一行不会编译。
type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK
在Go中,类型与方法紧密相关,因为每个命名类型都有一个(可能为空)方法集。一般规则是,您可以更改要转换的类型的名称(从而可能更改其方法集),但不能更改复合类型的元素的名称(和方法集)。Go要求您明确说明类型转换。
为什么我的nil错误值不等于nil?
在幕后,接口被实现为两个元素,即typeT 和value V。 V是一个具体值,例如int, struct或指针,永远都不是接口本身,并且具有type T。例如,如果我们将int值3存储在一个接口中,则结果接口值的示意图为(T = int,V = 3)。值V也称为接口的动态值,因为给定的接口变量在程序执行期间可能拥有不同的值V(和对应的类型T)。
仅当V和T都未设置(T = nil,未设置V)时,接口值才为nil。特别是,nil接口将始终保持nil类型。如果我们在接口值中存储* int类型的nil指针,则内部类型将是* int,而与指针的值无关:(T = * int,V = nil)。因此,即使当指针值V内部为nil时,该接口值也将为非nil。
这种情况可能会造成混淆,并且在接口值内部存储了nil值(例如错误返回)时会出现这种情况:
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
如果一切顺利,该函数将返回nil p,因此返回值将为错误接口值保持(T = * MyError,V = nil)。这意味着,如果调用者将返回的错误与nil进行比较,即使没有发生任何不良情况,它始终看起来像是有一个错误。要将正确的nil错误返回给调用者,该函数必须返回一个明确的nil:
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
对于总是返回错误的函数,最好始终在其签名中使用错误类型(如上所示),而不要使用诸如* MyError之类的具体类型,以确保错误被正确创建,这是一个好主意。例如,os.Open返回错误,即使它不是nil,也始终是具体类型* os.PathError。
每当使用接口时,都可能发生与此处描述的情况类似的情况。请记住,如果接口中已存储任何具体值,则该接口将不会为nil。有关更多信息,请参见 《反射定律》。
为什么没有像C中那样未加标签的联合?
未标记的联合会违反Go的内存安全保证。
为什么Go没有变体类型?
变体类型,也称为代数类型,提供了一种方法来指定值可以采用一组其他类型中的一种,但只能是那些类型。系统编程中的一个常见示例将指定错误为网络错误,安全错误或应用程序错误,并允许调用者通过检查错误的类型来区分问题的根源。另一个示例是语法树,其中每个节点可以是不同的类型:声明,语句,赋值等。
我们曾考虑将变体类型添加到Go中,但是经过讨论后,他们决定将它们排除在外,因为它们与接口的混淆方式相互重叠。如果变量类型的元素本身是接口,将会发生什么?
同样,该语言已经涵盖了某些变体类型所针对的内容。使用接口值来保存错误并使用类型开关来区分大小写,很容易表达错误示例。语法树示例也是可行的,尽管不是那么优雅。
为什么Go没有协变结果类型?
协变结果类型将意味着类似:
type Copyable interface {
Copy() interface{}
}
会满足于该方法
func (v Value) Copy() Value
因为Value实现了空接口。在Go中,方法类型必须完全匹配,因此Value不实现Copyable。Go从类型的实现中分离出类型的作用及其方法的概念。如果两个方法返回不同的类型,则它们不会做相同的事情。想要协变结果类型的程序员经常试图通过接口表达类型层次。在Go中,接口与实现之间的清晰分隔是很自然的。