golang-FAQ
new与make之间有什么不同?
new 分配内存,make 初始化切片、映射和信道类型
如何获得方法的动态分配?
拥有动态分配方法的唯一途径就是通过接口。结构或其它混合类型的方法总是静态地确定
如何编写单元测试?
在相同的目录中创建一个以 _test.go
结尾的新文件作为你的包源文件。 在该文件中,加入 import "testing"
并编写以下形式的函数:
func TestFoo(t *testing.T) {
...
}
在该目录中运行 go test
。该脚本会查找 Test
函数, 构建一个测试二进制文件并运行它。
闭包作为Go程在运行时会发生什么?
当闭包与并发一起使用时,可能会产生一些混乱。考虑以下程序:
func main() {
done := make(chan bool)
value := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// 在退出前等待所有Go程完成
for _ = range values {
<-done
}
}
有人可能会错误地希望看到 a, b, c
作为输出。而你可能会看到 c, c, c
。这是因为每一次循环迭代中都使用了变量 v
的相同实例,因此每一个闭包都共享了单一的变量。当该闭包运行时,它将在 fmt.Println
执行后打印出 v
的值,但 v
可能已经在Go程启动后被修改了。要在这类问题发生前发现它们,请运行 go vet
。
要将 v
的当前值在每一个闭包启动后绑定至它们,就必须在每一次迭代中, 通过修改内部循环来创建新的变量。其中一种方式就是将变量作为实参传至该闭包中:
for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
} (v)
}
在这个例子中,v
的值作为一个实参传入了该匿名函数。然后这个值就可作为变量 u
在该函数中访问了。
甚至只需简单地创建新的变量,使用声明的风格看起来可能有点怪,但这在Go中能很好地工作:
for _, v := range values {
v := v //创建新的"v"
go func() {
fmt.Println(v)
done <- true
} ()
}
为什么T与*T拥有不同的方法集?
根据Go规范中的定义
其它任意已命名类型
T
的方法集由所有带接收者类型T
的方法组成。 与指针类型*T
相应的方法集为所有带接收者*T
或T
的方法的集(就是说,它也包含T
的方法集)。
如果一个接口值包含一个指针 *T
,一个方法调用可通过解引用该指针来获得一个值, 但如果一个接口值包含一个值 T
,就没有可用的方式让一个方法调用获得一个指针。
即便在编译器可以获得传入方法的值的地址的情况下,若该方法修改了该值,则更改会在调用者中丢失。 一个常见的例子是
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
会将标准输入复制到 buf
的副本中,而不是复制到 buf
自身。这几乎是从不期望的行为。
应当为值或指针定义方法吗?
func (s *MyStruct) pointerMethod() {} //为指针定义的方法
func (s MyStruct) valueMethod() {} //为值定义的方法
当为一个类型定义了一个方法,则接收者(上面例子中的 s
)的表现正好就是该方法的实参。 将接收者定义为值还是指针都是一样的问题,就像一个函数的实参应该是值还是指针一样。 有几点需要考虑的地方。
首先,也是最重要的一点,方法需要修改接收者吗?如果是,则接收者必须是一个指针。 (切片和映射的行为类似于引用,所以它们的状况有一点微妙,但比如说要改变方法中切片的长度, 则接受者仍然必须是指针。)在上面的例子中,如果 pointerMethod
修改了 s
的字段,那么调用者将观察到那些改变,但 valueMethod
是由调用者实参的副本调用的(这是传值的规定),因此对它的更改对于调用者来说是不可见的。
顺便一提,指针接收者在Java中的情况和Go是相同的,尽管在Java中指针隐藏在幕后; 而Go的值接受者则不相同。
其次是效率问题的考虑。若接受者很大,比如说一个大型的 struct
, 使用指针接收器将更廉价。
接着是一致性问题。若某些类型的方法必须拥有指针接收者,则其余的也应该这样, 因此不管该类型被如何使用,方法集都始终如一。更多详情见方法集一节。
对于诸如基本类型、切片以及小型 struct
这样的类型,值接收者是非常廉价的, 因此除非该方法的语义需要一个指针,一个有效而清楚的值接收者。
函数形参在什么时候传值?
和所有C家族中的语言一样,Go中的所有东西都通过值来传递。也就是说, 函数总是会获得向它传递的东西的一份副本,就好像有一个赋值语句向它的形参赋值。 例如,将一个 int
值传入一个函数就会创建该 int
值的一份副本,而传入一个指针值则会创建该指针的一份副本,而不是它所指向的数据。 (关于它如何影响方法接收器的讨论见下一节。)
映射和切片值的行为就像指针一样:它们就是包含指向基本映射或切片数据的指针的描述符。 复制一个映射或切片会创建一个存储在接口值中的东西的一个副本。若该接口值保存了一个结构, 复制该接口值则会创建一个该结构的副本。若该接口值保存了一个指针, 复制该接口值则会创建一个该指针的副本,而且同样不是它所指向的数据。
为什么我的nil错误值不等于nil?
在底层,接口作为两个元素实现:一个类型和一个值。该值被称为接口的动态值, 它是一个任意的具体值,而该接口的类型则为该值的类型。对于 int
值3, 一个接口值示意性地包含(int
, 3
)。
只有在内部值和类型都未设置时(nil
, nil
),一个接口的值才为 nil
。特别是,一个 nil
接口将总是拥有一个 nil
类型。若我们在一个接口值中存储一个 *int
类型的指针,则内部类型将为 *int
,无论该指针的值是什么:(*int
, nil
)。 因此,这样的接口值会是非 nil
的,即使在该指针的内部为 nil
。
这种情况会让人迷惑,而且当 nil
值存储在接口值内部时这种情况总是发生, 例如错误返回:
func returnError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p //将总是返回一个非nil错误
}
如果一切顺利,该函数会返回一个 nil
的 p
, 因此该返回值为拥有(*MyError
, nil
)的 error
接口值。这也就意味着如果调用者将返回的错误与 nil
相比较, 它将总是看上去有错误,即便没有什么坏事发生。要向调用者返回一个适当的 nil
error
,该函数必须返回一个显式的 nil
:
func returnError() error {
if bad() {
return ErrBad
}
return nil
}
这对于总是在签名中使用 error
类型返回错误(正如我们上面做的)而非像 *MyError
这样具体类型的函数来说是个不错的主意,它可以帮助确保错误被正确地创建。 例如,即使 os.Open
返回一个 error
, 若非 nil
的话,它总是具体的类型 *os.PathError
。
对于那些描述,无论接口是否被使用,相似的情形都会出现。只要记住,如果任何具体的值已被存储在接口中, 该接口就不为 nil
。
能否将[]T转换为[]interface{}?
不能直接转换,因为它们在内存中的表示并不相同。必须单独地将元素复制到目标切片。 下面的例子将 int
切片转换为 interface{}
切片:
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
为什么类型T不满足Equal接口?
考虑以下简单的接口,它表示一个可以将自身与另一个值进行比较的对象:
type Equaler interface {
Equal(Equaler) bool
}
以及此类型 T
:
type T int
func (t T) Equal(u T) bool { return t == u } // does not satisfy Equaler
不像在一些多态类型系统中类似的情况,T
并未实现 Equaler
。 T.Equal
的实参类型为 T
,而非字面上所需要的类型 Equalar
。
在Go中,类型系统并不提升 Equal
的实参,那是程序员的责任, 就以下类型 T2
所示,它实现了 Equaler
:
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } //满足Equaler
即使它不像其它的类型系统也好,因为在Go中任何满足 Equaler
的类型都能作为实参传至 T2.Equal
,并在运行时我们必须检查该实参是否为 T2
类型。一些语言将其安排在编译时以保证做到这一点。
一个相关的例子是另一种情形:
type Opener interface {
Open() Reader
}
func (t T3) Open() *os.File
在Go中,T3
并不满足 Opener
,尽管它在另一种语言中可能满足。
在相同情况下,Go的类型系统确实为程序员做的更少, 子类型化的缺乏使关于接口满足的规则非常容易制订: 函数的名字和签名完全就是那些接口吗?Go的规则也容易高效地实现。 我们感觉这些效益抵消了自动类型提升的缺失。Go在某天应当采取一些泛型的形式, 我们期望会有一些方式来表达这些例子的想法,且也拥有静态检查。
如何保证我的类型满足某个接口?
可以通过尝试赋值来要求编译器检查类型 T
是否实现了接口 I
:
type T struct{}
var _ I = T{} //确认T是否实现了I。
如果你希望接口的使用者显式地声明它们实现了它,你可以将一个带描述性名称的方法添加到该接口的方法集中:
type Fooer interface {
Foo()
ImplementsFooer()
}
然后类型必须实现 ImplementsFooer
方法成为 Fooer
, godoc的输出中清晰地记录了事实和通告。
type Bar struct{}
func (b Bar) ImplemnetsFooer() {}
func (b Bar) Foo() {}
大部分代码无需使用这类约束,因为它们限制了实用程序的接口思想。 但有时候,它们也需要解决相似接口之间的歧义。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2018-11-26 基本数据类型大小和范围