65道Go基础高频题整理(附答案背诵)
说明一下Golang 中 make 和 new 的区别?
好的,关于 make
和 new
在 Go 语言中的区别,我来解释一下。
-
new
函数的作用:new(T)
函数会为 T 类型的新项分配零值内存,并返回其地址,即一个*T
类型的值(指针)。这种方法适用于各种类型,包括基本类型如int
、float
、bool
等,以及数组、结构体等。- 使用
new
分配的内存被初始化为类型的零值,例如new(int)
返回的指针指向的值会被初始化为 0。
-
make
函数的作用:make
函数仅用于切片(slice)、映射(map)和通道(channel)的内存分配,并返回一个有初始值(非零值)的 T 类型,而不是*T
。- 对于切片,
make
会分配内存并初始化切片的内部结构,如长度和容量等。 - 对于映射,
make
会初始化映射的结构。 - 对于通道,
make
会配置通道的缓冲大小。
-
应用场景与例子:
- 使用
new
:- 当你需要一个指向某基本类型零值的指针时,比如在需要在函数间共享或者更改数据时。
- 例子:
num := new(int)
创建了一个指向整数的指针,其值初始化为 0。
- 使用
make
:- 创建切片、映射或通道时,并且需要它们立即可用,例如要在切片中存储数据,或者在映射中设置键值对。
- 例子:
s := make([]int, 10)
创建了一个长度和容量都是 10 的整数切片。
- 使用
总结:new
和 make
都是用于分配内存,但 new
返回的是指针,且适用于所有类型,而 make
只用于切片、映射和通道,并返回一个初始化的(非零值的)类型实例。
说明一下 Golang 数组和切片的区别?
Golang 中数组和切片是两种不同的数据类型,它们有以下几个主要区别:
-
长度固定性:
- 数组:数组的长度是固定的,定义时必须指定长度,并且一旦定义,其长度不能更改。
- 切片:切片是动态的,长度可以改变。切片实际上是对数组的抽象。
-
声明方式:
- 数组:声明时需要指定长度,例如
var a [5]int
定义了一个包含 5 个整数的数组。 - 切片:在声明切片时,不需要指定长度,例如
var s []int
是一个切片,初始时为空。
- 数组:声明时需要指定长度,例如
-
内部结构:
- 数组:数组直接存储值,数组的大小和元素类型一起定义了数组的类型。
- 切片:切片包含三个元素:一个指向底层数组的指针,切片的长度,以及切片的容量。因此,切片比数组更加灵活。
-
传递方式:
- 数组:在函数调用时,数组是通过值传递的,这意味着传递的是整个数组的副本。
- 切片:切片是通过引用传递的,所以函数内部对切片的修改会影响到原切片。
-
使用场景:
- 数组:当你需要固定长度的序列时使用数组。例如,你知道你只需要存储 12 个月的名称,可以使用数组。
- 切片:更加常用,特别是当你需要一个可伸缩的列表时。例如,你在处理一个用户列表,这个列表可能会根据用户注册的增减而变化。
-
性能考虑:
- 数组:由于大小固定,内存分配在栈上,因此访问速度快。
- 切片:更加灵活,但需要小心处理容量和长度的变化,可能会涉及到内存重新分配。
总结:数组是静态的、固定长度的序列,而切片是动态的、可以伸缩的序列。切片在 Go 中更加常用,提供了更高的灵活性和便利性。
使用for range 的时候,它的地址会发生变化吗?
在 Go 语言中,for range
循环的行为取决于你正在遍历的数据类型。对于数组或切片,for range
会提供每个元素的副本,而不是直接引用原始元素。因此,如果你尝试获取这些元素的地址,你会发现地址在每次迭代中都会改变,因为你实际上获取的是副本的地址。
这点可以通过以下的例子来说明:
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf("Index: %d, Value: %d, Address: %p\n", i, v, &v)
}
在上述代码中,v
是数组元素的副本,而不是直接引用。因此,&v
在每次迭代中都会产生不同的地址,这些地址指向的是副本的位置,而不是原始数据的位置。
但是,如果你直接操作原始数组(或切片)的元素,例如:
nums := []int{10, 20, 30}
for i := range nums {
fmt.Printf("Index: %d, Value: %d, Address: %p\n", i, nums[i], &nums[i])
}
在这个例子中,&nums[i]
将在每次迭代中提供原始元素的地址,这些地址在每次迭代中都是不变的,因为它们直接引用了原始数据。
总结:在 for range
循环中,如果尝试获取每次迭代中提供的元素的地址,这些地址会在每次迭代中改变,因为这些元素是原始数据的副本。如果你需要操作原始数据的地址,你应该直接引用原始数据。
go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?
defer
关键字在Go语言中用于确保函数调用在程序执行完毕后,无论函数是否出现错误,都能正确地被执行。当有多个defer
语句在同一函数中,他们会以LIFO(后进先出)的顺序执行。这就意味着在同一个函数内,最后声明的defer
语句会被最先执行。
关于修改返回值,defer
语句在函数返回之后执行,但是它可以访问并修改返回值。这是因为返回值在函数结束时会被当作defer
语句的参数。这意味着如果你在defer
函数中修改了返回值,那么实际的返回值会被改变。
以下是一个简单的例子来解释这个概念:
package main
import "fmt"
func main() {
fmt.Println(deferTest())
}
func deferTest() (result int) {
defer func() {
result++
}()
return 0
}
在这个例子中,deferTest
函数的返回值在没有defer
语句的情况下应该是0
。但是,因为我们在defer
语句中将result
增加了1
,所以最终返回的值实际上是1
。当我们运行main
函数,输出结果就是1
,而不是0
。
简要说明一下Golang 单引号,双引号,反引号的区别?
在 Go 语言中,单引号 ('
), 双引号 ("
), 和反引号 (```) 都被用于表示字符串,但是它们的用途和行为有所不同:
-
单引号 (' ') :用于表示单个字符(rune)。它包含的字符必须是 Unicode 编码的字符,例如
'a'
、'中'
或者 Unicode 编码'u4E2D'
。不能用于表示字符串。例如:
var char rune = 'a' ```
-
双引号 (" ") :用于表示字符串。字符串是 UTF-8 字符的序列。字符串内的特殊字符可以通过反斜杠 (
\
) 进行转义。例如:
var str string = "Hello, World!\n" ```
-
反引号 (` `) :也用于表示字符串,但是反引号表示的字符串会保留原始内容,包括换行和其他特殊字符,不支持任何转义序列。
例如:
var str string = `Hello, World!` ```
在这个例子中,反引号字符串会保留原始的换行符。
总结:单引号用于表示字符,双引号和反引号用于表示字符串,但双引号支持转义序列,而反引号保留字符串的原始格式。
Go的函数与方法及方法接受者区别?
在Go语言中,函数和方法都是用来封装代码的,但它们在使用方式上有一些不同。
函数是具有一组输入参数和返回值的独立实体,它不依赖于任何类型。例如:
func Add(a int, b int) int {
return a + b
}
在这个例子中,Add
是一个函数,它接受两个整数作为输入,然后返回两个整数的和。
而方法是依赖于特定类型的函数,这个特定类型被称为方法的接收者。例如:
type MyInt int
func (m MyInt) Add(otherInt int) int {
return int(m) + otherInt
}
在这个例子中,Add
是一个方法,它的接收者是MyInt
类型。这意味着我们可以在任何MyInt
类型的实例上调用Add
方法。
方法接收者可以是任何类型的值或者指针,不过不能是一个接口类型或者一个指针类型。接收者的类型在方法名前,以(接收者) 方法名
的形式出现。
方法接收者有两种类型:值接收者和指针接收者。值接收者方法在调用时,接收者的值会被复制,而指针接收者在调用方法时,会使用接收者的实际地址,所以可以修改接收者的值。
type MyInt int
func (m MyInt) valueReceiver() { // 值接收者
m = 5
}
func (m *MyInt) pointerReceiver() { // 指针接收者
*m = 5
}
func main() {
var num MyInt = 2
num.valueReceiver()
fmt.Println(num) // 输出 2
num.pointerReceiver()
fmt.Println(num) // 输出 5
}
在这个例子中,你会看到valueReceiver
方法没有改变num
的值,而pointerReceiver
方法改变了num
的值。
能详细介绍一下Golang中的defer底层数据结构和特性吗?
在 Go 语言中,defer
关键字用于推迟一个函数或方法的执行,直到包含该 defer
语句的函数执行完成。这个被延迟的函数被称为 "deferred function"。
defer
的主要特性包括:
-
后进先出(LIFO):当在一个函数中存在多个
defer
语句时,它们将会以后进先出的顺序执行。也就是说,最后一个defer
语句最先被执行,第一个defer
语句最后被执行。 -
参数在 defer 语句中立即求值:在
defer
语句中,函数的参数会立即被计算并保存,而函数本身的执行会被延迟。 -
延迟函数的执行时机:
defer
的函数会在包含defer
语句的函数返回之前执行,无论这个函数是通过 return 正常结束,还是由于 panic 导致的异常结束。
关于 defer
的底层实现,Go 运行时使用了一个叫做 "deferred function stack" 的结构来管理 defer
调用。这是一个后进先出(LIFO)的栈结构,每当遇到 defer
调用,运行时就会将其添加到当前 goroutine 的 defer 栈中。每个栈帧包含了被推迟函数的指针以及其参数,这些参数在 defer
语句被执行时就已经被求值。
当包含 defer
语句的函数即将返回时,运行时会从 defer 栈中弹出一个栈帧,并执行其中的函数。如果有多个 defer
语句,那么就会按照后进先出的顺序依次执行。
总之,defer
提供了一种强大的机制,可以用来处理资源的清理工作,如关闭文件句柄、解锁互斥锁等,无论函数由于何种原因结束,都能保证资源的正确清理。
能否介绍一下Golang中的slice底层数据结构和特性?
Go语言中的切片(slice)是一种动态数组,它的底层数据结构包括三个关键的属性:指针、长度和容量。
- 指针(Pointer):指向数组的第一个元素。
- 长度(Length):当前切片的长度,即包含的元素数量。
- 容量(Capacity):从切片的开始位置到底层数组的结尾位置的元素数量。
Go语言的切片是引用类型,它们不存储任何数据,只描述底层数组的一段。更改切片的元素会修改其底层数组中的对应元素。
切片的长度和容量可以通过内置的 len()
和 cap()
函数获取。您可以通过 append()
函数向切片添加元素。如果添加的元素数量超过切片的容量,append()
会创建一个新的底层数组,并将原始数据复制到新数组中,然后返回新的切片。
这是一个简单的例子:
package main
import "fmt"
func main() {
// 创建一个切片
s := []int{1, 2, 3}
fmt.Println(len(s), cap(s)) // 输出:3 3
// 添加一个元素
s = append(s, 4)
fmt.Println(len(s), cap(s)) // 输出:4 6
}
在这个例子中,我们首先创建了一个包含三个元素的切片。然后,我们向切片添加了一个元素,因为切片的容量不足以容纳新的元素,所以 append()
创建了一个新的底层数组,并将原始切片的数据复制到新数组中,然后返回了新的切片。新切片的容量是原始切片的容量的两倍。
在Golang中,如何高效地拼接字符串?
在Go语言中,最高效的方式拼接字符串是使用strings.Builder
或bytes.Buffer
。这两者都能够避免在拼接过程中多次创建和复制字符串,因此它们比普通的+
或+=
运算符更加高效。
以下是一个使用strings.Builder
的例子:
package main
import (
"strings"
"fmt"
)
func main() {
var builder strings.Builder
for i := 0; i < 10; i++ {
builder.WriteString("Go ")
}
result := builder.String() // Get the concatenated string
fmt.Println(result)
}
在这个例子中,我们创建了一个strings.Builder
对象,并使用WriteString
方法向其写入字符串。最后,我们调用String
方法获取最终拼接的字符串。
同样,我们可以使用bytes.Buffer
达到类似的效果:
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
for i := 0; i < 10; i++ {
buffer.WriteString("Go ")
}
result := buffer.String() // Get the concatenated string
fmt.Println(result)
}
这两种方式都可以有效地拼接字符串,而不会像+
或+=
那样造成大量的内存分配和复制。在处理大量字符串拼接时,这可以带来显著的性能提升。
Golang中2 个 interface 可以比较吗?
在 Go 语言中,两个接口类型的变量可以比较,但有一些规则和限制:
-
nil 接口比较:两个 nil 接口值是相等的。
-
非 nil 接口比较:如果两个接口值的动态类型相同,并且动态值也相等,那么这两个接口值就被认为是相等的。
-
限制:如果接口的动态值是不可比较的类型(如切片),那么在尝试比较接口值时将会导致运行时错误。
下面是一些示例:
type Data struct {
num int
}
var i, j interface{}
fmt.Println(i == j) // 输出:true
i = 10
j = 10
fmt.Println(i == j) // 输出:true
i = Data{num: 10}
j = Data{num: 10}
fmt.Println(i == j) // 输出:true
i = []int{1, 2, 3}
j = []int{1, 2, 3}
fmt.Println(i == j) // 运行时错误:slices can only be compared to nil
总的来说,可以比较两个接口值,但你需要确保接口的动态值是可比较的类型,否则会导致运行时错误。
Golang中init() 函数是什么时候执行的?
在 Go 语言中,init()
函数是一种特殊的函数,它在每个包完成初始化后自动执行。这意味着,你不能在代码中显式地调用 init()
函数,它由 Go 运行时系统自动调用。初始化顺序为:
-
如果一个包被导入多次,
init()
函数只会被执行一次。 -
包的初始化顺序为:首先初始化包级别(Package Level)的变量,然后是
init()
函数。如果一个包导入了其他包,会先初始化被导入的包。 -
即使一个包被多个其他包导入,它的
init()
函数也只会被执行一次。 -
在同一个包内,多个
init()
函数的执行顺序为它们在 Go 语言中,init()
函数是一种特殊的函数,它在每个包完成初始化后自动执行。这意味着,你不能在代码中显式地调用init()
函数,它由 Go 运行时系统自动调用。初始化顺序为: -
如果一个包被导入多次,
init()
函数只会被执行一次。 -
包的初始化顺序为:首先初始化包级别(Package Level)的变量,然后是
init()
函数。如果一个包导入了其他包,会先初始化被导入的包。 -
即使一个包被多个其他包导入,它的
init()
函数也只会被执行一次。 -
在同一个包内,多个
init()
函数的执行顺序为它们在代码中的出现顺序。
这是一个简单的例子:
package main
import "fmt"
var foo = initFoo()
func initFoo() int {
fmt.Println("Initializing variable...")
return 42
}
func init() {
fmt.Println("Executing init function...")
}
func main() {
fmt.Println("Executing main function...")
fmt.Println("Foo:", foo)
}
在这个例子中,程序的输出会是:
Initializing variable...
Executing init function...
Executing main function...
Foo: 42
这显示了 Go 语言初始化的顺序:首先初始化包级别的变量(在这里是 foo
),然后执行 init()
函数,最后是 main()
函数。
Golang中如何比较两个 map 是否相等?
Go语言的标准库中没有直接提供比较两个map
是否相等的函数,所以我们需要自己编写函数来实现这个功能。在比较两个map
是否相等时,我们需要检查两个map
的长度是否相等,然后检查每个键在两个map
中都存在,并且对应的值也相等。
下面是一个简单的函数,用于比较两个map[string]int
是否相等:
package main
import "fmt"
func mapsEqual(a, b map[string]int) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if vb, ok := b[k]; !ok || vb != v {
return false
}
}
return true
}
func main() {
map1 := map[string]int{"one": 1, "two": 2}
map2 := map[string]int{"one": 1, "two": 2}
fmt.Println(mapsEqual(map1, map2)) // 输出 true
}
在这个例子中,mapsEqual
函数首先检查两个map
的长度是否相等。然后,它遍历map a
的每个键值对,检查该键在map b
中是否存在,以及map b
中的值是否与map a
中的值相等。如果所有键值对都匹配,那么函数就返回true
,表示两个map
相等。否则,返回false
。
是否可以对Golang中的map元素取地址?
在Go语言中,map
的元素并不是变量,因此你不能直接获取map
元素的地址。尝试这样做会导致编译错误。例如,以下代码将无法编译:
package main
import "fmt"
func main() {
myMap := map[string]int{
"foo": 42,
}
addr := &myMap["foo"] // 编译错误:cannot take the address of myMap["foo"]
fmt.Println(addr)
}
这是因为map
的元素可能会因为新元素的添加或者map
的扩容而被移动,所以直接获取map
元素的地址可能会引用到错误的元素。
如果你需要修改map
中的元素,你可以使用替换的方式:
package main
import "fmt"
func main() {
myMap := map[string]int{
"foo": 42,
}
myMap["foo"] = 43
fmt.Println(myMap) // 输出:map[foo:43]
}
如果你需要一个可以取地址的map
元素,你可以将map
的值设置为一个包含所需元素的结构体或者指向该元素的指针:
package main
import "fmt"
func main() {
myMap := map[string]*int{}
val := 42
myMap["foo"] = &val
addr := myMap["foo"]
fmt.Println(*addr) // 输出:42
}
在这个例子中,我们创建了一个map
,它的值是整数指针。然后,我们可以获取和修改这些值的地址。
在Golang中,Map可以边遍历边删除元素吗?
在 Go 语言中,可以在遍历 map 的过程中删除元素。Go 的官方文档明确指出:“在迭代过程中,删除迭代器返回的映射项是安全的。”这是因为,删除操作不会影响迭代器的状态。
以下是一个例子:
m := map[int]string{
1: "a",
2: "b",
3: "c",
4: "d",
}
for k := range m {
if k == 1 {
delete(m, k)
}
}
在这个例子中,我们在遍历过程中删除了键为 1 的元素,这是完全安全的。
然而,需要注意的是,map 的遍历顺序在 Go 中是不确定的。所以,你不能预测或依赖于遍历的顺序,除非你使用某种方式对键进行排序。
Golang中的float类型可以作为Map的key吗?
在 Go 语言中,浮点类型(float32
和 float64
)可以作为 map 的键(key),但这并不是一个好的实践。原因在于浮点数的精度问题和比较的复杂性。
浮点数的比较可能会引入微小的误差,这可能导致意料之外的结果。即使两个浮点数看起来相等,但由于精度问题,它们可能在内存中的表示是不同的。这就意味着,即使你使用看似相同的键来访问 map,你可能得到不同的结果。
这是一个例子:
package main
import "fmt"
func main() {
m := make(map[float64]string)
m[0.1+0.2] = "value1"
m[0.3] = "value2"
fmt.Println(m[0.1+0.2]) // 输出:"value1"
fmt.Println(m[0.3]) // 输出:"value2"
}
尽管 0.1+0.2
和 0.3
在数学上是相等的,但在浮点数的世界中,由于精度问题,它们可能不相等。所以,尽管它们看起来应该映射到同一个值,但在这个例子中,它们映射到了两个不同的值。
因此,通常建议使用更稳定、更可预测的数据类型(例如整型或字符串)作为 map 的键。
请问Golang中的map的key为什么是无序的?
Go 语言中的 map 数据结构是基于哈希表实现的。哈希表是一种数据结构,它通过使用哈希函数将键(key)映射到存储位置。这种映射过程使得查找元素的速度非常快,几乎与 map 的大小无关。
然而,这种映射过程并不保证元素的顺序。当你向 map 添加新元素时,如果发生哈希冲突(即两个或更多的 key 被哈希到同一个存储位置),哈希表可能需要重新分配更多的空间,并重新哈希所有的元素以避免冲突。这个过程可能会导致元素的顺序发生变化。
此外,Go 还有意地在每次运行程序时使用不同的哈希种子,以增加 map 的安全性并避免某些类型的攻击。这意味着即使你使用相同的键集合,每次运行程序时 map 的元素顺序也可能会改变。
因此,Go 语言中的 map 数据结构并不保证元素的顺序,遍历 map 的结果是无序的。如果你需要有序的键值对,你可能需要使用其他的数据结构,如排序的切片或者专门的有序 map 库。
能介绍一下Golang中的map的扩容机制吗?
Go语言的map
实际上是一个哈希表,它的大小会动态变化。当map
的元素数量达到一定阈值时,就会触发扩容操作,即重新分配更大的内存空间,并将所有的元素重新哈希到新的内存空间中。
具体来说,Go语言的map
扩容规则如下:
-
初始的
map
大小是0。当第一次添加元素时,map
的大小会增加到8。 -
当
map
的元素数量超过其大小的6.5倍时,或者已经存储的元素数量超过了2^15(32768)时,map
的大小会翻倍。 -
map
的最大大小受限于地址空间的大小和指针的大小。在32位系统中,最大大小约为231,而在64位系统中,最大大小约为250。
这种设计使得map
在添加新元素时仍能保持较好的性能,但也意味着添加元素可能需要重新哈希所有的元素,这可能会暂时阻塞map
的其他操作。
因此,如果你知道最终需要存储的元素数量,那么在创建map
时预先设置一个足够大的容量会更加高效,因为这样可以避免多次扩容操作。例如:
myMap := make(map[string]int, 10000)
以上代码创建了一个预期容量为10000的map
,如果实际元素数量不超过这个值,那么就不会触发扩容操作。
Golang中Map的数据结构是什么?
Go语言中的 map
是一种哈希表数据结构,也就是键值对的集合。它的底层实现包括数组、哈希函数、链接等关键组成部分。
-
数组(Array):数组是
map
的核心组成部分,用于存储键值对(bucket)。每个 bucket 可以存储一到八个键值对。 -
哈希函数(Hash Function):哈希函数接收一个键并返回一个整数。这个整数被用来确定键值对应该存储在哪个 bucket 中。
-
链接(Linking):如果两个或更多的键哈希到同一个 bucket,那么它们会被链接在一起。这就是所谓的哈希冲突。为了解决这个问题,Go 的
map
使用了链接技术,也就是说,它在同一个 bucket 中存储了一个键值对的链表。
以下是一个简单的例子:
package main
import "fmt"
func main() {
// 创建一个空的 map
m := make(map[string]int)
// 添加键值对
m["apple"] = 1
m["banana"] = 2
// 获取和打印键值对
fmt.Println(m["apple"]) // 输出:1
fmt.Println(m["banana"]) // 输出:2
}
在这个例子中,我们创建了一个空的 map
,然后添加了两个键值对。当我们从 map
中获取一个值时,Go 使用哈希函数确定键的哈希值,然后在对应的 bucket 中查找键值对。
需要注意的是,Go 的 map
是动态大小的。当向 map
中添加更多的键值对时,如果当前的数组容量不足以容纳更多的数据,Go 就会创建一个新的、更大的数组,并把旧的数据复制到新的数组中。这个过程被称为重新哈希(rehashing)。
在Golang中,任意类型T()都能够调用*T的方法吗?反过来呢?
在 Go 语言中,对于一个给定的类型 T
,接收者为 T
的方法可以被 T
类型的值和 *T
类型的指针调用。这是因为 Go 在尝试匹配方法接收者的类型时会自动做取址或解引用的操作。
例如:
type MyType int
func (m MyType) ValueMethod() {
fmt.Println("ValueMethod called")
}
func (m *MyType) PointerMethod() {
fmt.Println("PointerMethod called")
}
var v MyType
var p = &v
v.ValueMethod() // 正常调用
p.ValueMethod() // 正常调用,Go 自动将 p 解引用
v.PointerMethod() // 正常调用,Go 自动取 v 的地址
p.PointerMethod() // 正常调用
然而,如果 T
是一个接口类型,情况就有所不同。接口类型的值不能调用接口类型的指针的方法。
总的来说,对于非接口类型 T
和 *T
,它们可以调用彼此的方法。但对于接口类型,值类型接口不能调用指针类型接口的方法。这是因为接口类型的值和指针遵循不同的规则。
由于内容太多,更多内容以链接形势给大家,点击进去就是答案了
20. 请问在Golang中,函数返回局部变量的指针是否安全?
24. Golang的切片作为函数参数是值传递还是引用传递?
26. Golang中nil map 和空 map 的区别是什么?
27. 在Golang中,删除一个key后,它的内存会被释放吗?
29. Golang 调用函数传入结构体时,应该传值还是指针?
43. 请说一下Go 中 uintptr 和 unsafe.Pointer 的区别?
44. 简述一下Golang空结构体 struct{} 的使用 ?
47. Switch 中如何强制执行下一个 case 代码块 ?
52. 阐述一下Printf()、Sprintf()、Fprintf()函数的区别和用法?
53. 阐述一下Go 如何Array 类型的值作为函数参数 ?
58. nil interface 和 nil interface 有什么区别 ?
59. Golang导入包时,为什么可能使用’ _’ /’ .'导入? 举例说明
60. 在Golang中,接口类型是否支持像Java那样的多继承?