《100 Go Mistakes and How to Avoid Them》之函数篇
礼物说6.1 不知该用哪种类型的receiver
什么时候使用值类型接收,什么时候使用指针类型接收?
type customer struct {
float64
balance }
func (c customer) add(v float64) {
.balance += v
c}
func (c *customer) add(v float64) {
.balance += v
c}
值类型接收相当于把receiver
的值拷贝传递进去,在内部改变,不会影响原值。而指针类型接收相当于把指针的拷贝传递进去,会改变原值。什么时候一定要用 指针类型?
这个方法需要改变
receiver
的值 或者 receiver是slice 需要appendreceiver
中存在不能copy的字段 比如:sync
package里的类型
什么时候应该使用 指针类型?
receiver
是一个巨大的对象,使用指针可以避免内存分配。
什么时候一定要用 值类型?
我们需要强制
receiver
不可改变receiver
是map/function/channel的时候。
什么时候应该使用 值类型?
receiver
是一个slice且不需要改变的时候receiver
是一个小型的数组或者结构体,并且都是值类型没有可变字段,比如time.Time的时候receiver
是基础类型比如int, float64, string的时候
6.2 不使用已命名的返回参数
已命名的返回参数可以提高我们代码的可读性,有时也能时函数更简短。比如
type locator interface {
(address string) (float32, float32, error)
getCoordinates}
以上函数返回一个地址的经纬度,你如何确定第一第二个参数哪个是经度哪个是纬度?这种使用就需要用到提前命名。
type locator interface {
(address string) (lat, lng float32, err error)
getCoordinates}
这样就能通过签名很清晰的使用这个interface。另外一个例子出自Effective Go,描述了io.ReadFull
这个函数
func ReadFull(r io.Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
, err = r.Read(buf)
nr+= nr
n = buf[nr:]
buf }
return
}
出参提前命名好,内部实现就会更简短,逻辑更清晰。
6.3 已命名的返回参数的意外边界情况
已命名的返回参数
在某些情况下很有用,但如果不仔细有时候会导致一些细小的bug。
func (l loc) getCoordinates(ctx context.Context, address string) ( lat, lng float32, err error) {
:= l.validateAddress(address)
isValid if !isValid {
return 0, 0, errors.New("invalid address")
}
if ctx.Err() != nil {
return 0, 0, err
}
// Get and return coordinates
}
比如上述例子,err根本没赋值,但因为已命名的返回方式,他就不会报编译错误。我们可以使用局部变量来赋值。
if err := ctx.Err(); err != nil {
return 0, 0, err
}
6.4 返回了一个为nil的receiver
我们可以先看一个例子:
type MultiError struct {
[]string
errs }
func (m *MultiError) Add(err error) {
.errs = append(m.errs, err.Error())
m}
func (m *MultiError) Error() string {
return strings.Join(m.errs, ";")
}
func (c Customer) Validate() error {
var m *MultiError
if c.Age < 0 {
= &MultiError{}
m .Add(errors.New("age is negative"))
m}
if c.Name == "" {
if m == nil {
= &MultiError{}
m }
.Add(errors.New("name is nil"))
m}
return m
}
:= Customer{Age: 33, Name: "John"}
customer if err := customer.Validate(); err != nil {
.Fatalf("customer is invalid: %v", err)
log}
我们会发现一个很神奇结果触发了err != nil
,但是输出却是2021/05/08 13:47:28 customer is invalid: <nil>
其实在Go中,结构体函数只是一种语法糖(receiver
作为函数的第一个入参),所以一个空指针作为receiver
也是合法的。所以例子中return的结果是一个空指针而不是nil值。我们可以在最后判断下:
func (c Customer) Validate() error {
var m *MultiError
if c.Age < 0 {
// ...
}
if c.Name == "" {
// ...
}
if m != nil {
return m
}
return nil
}
6.5 使用一个filename作为入参
当我们创建一个需要读文件的函数时,传递一个文件名作为入参并不合适,比如单元测试的时候就很难写。比如我们编写一个读取行数的函数,我们需要覆盖3种case:正常case/空文件/只有空行的文件。那我们就需要创建3个文件在单元测试中。后续也有可能需要读取request?读取socket?。所以比较好的方式是使用io.Reader
作为参数。
6.6 忽略defer
参数和receiver
是如何计算的
func f() error {
var status string
defer notify(status)
defer incrementCounter(status)
if err := foo(); err != nil {
= StatusErrorFoo
status return err
}
if err := bar(); err != nil {
= StatusErrorBar
status return err
}
= StatusSuccess
status return nil
}
已上例子有什么问题?其实两个defer
传递的status
都是空字符串,因为defer
的参数计算是立即计算的,而并不是在ruturn的那个时刻。
方案一我们可以改成:
defer notify(status)
defer incrementCounter(status)
传递指针,这样计算的是指针的地址,但是这样需要改变函数的签名,有时候不太合适。我们也可以采用另外的方式:使用闭包。
defer func() {
(status)
notify(status)
incrementCounter}()
另一种比较容易混淆的就是结构体函数的defer
调用。
func main() {
:= Struct{id: "foo"}
s defer s.print()
.id = "bar"
s}
type Struct struct {
string
id }
func (s Struct) print() {
.Println(s.id)
fmt}
值传递输出的结果是foo
,指针传递输出的结果是bar
.