笔记

42.结构体方法不知道使用哪种类型的接收者

文中说了很多,感觉为了凑数,主要就是围绕着值传递还是指针传递,值传递给接收者,如果结构体中没有指针类型成员,则不会改变接收者的数据;如有指针传递,则会改变。选择哪种传递方式,主要取决于结构体中的成员是否都是只读操作,如果是只读,使用值传递,否则使用指针传递。至于是否大结构体数据,这个由结构体中的成员来决定,是否有指针成员;

43.从不使用命名结果参数 (建议)

主要是为了函数/结构体方法返回的值,更具代码可读性,数据类型本身不能表示具体含义,除非type 别名; 在大多数情况下,在接口定义的上下文中使用命名结果参数可以提高可读性而不会导致任何副作用。但是在方法实现的上下文中没有严格的规则。 当有明显的好处时,应该谨慎使用命名结果参数。

44.具有命名结果参数的意外副作用 (建议)

如果使用命名的结构参数,声明了结果的变量名,在函数方法体内进行初始定义,使用时注意不用被同名覆盖,特别是在处理错误逻辑时,错误的返回情况,所以 使用命名结果参数时保持谨慎,以避免潜在的副作用 。

45.返回一个 nil 接口 (重要)

在处理自定义实现error接口的错误体结构,判断错误时需要注意的地方,如果接口error 为未赋值的自定义错误体结构,虽然错误体结构为nil,但是interface error不为nil, 其interface error 内部指向的错误体结构为nil, 所以需要判断erro为nil时, 直接返回nil值, 而不是为nil的错误体结构体指针;这个是Go中经常会遇到的坑。这种情况也可能发生在任何使用指针接收者实现的接口,需要保持谨慎。

46.使用文件名作为函数输入

这个是属于设计问题了,函数操作的是共性数据时,应该采用接口作为函数的输入,以便解耦具体实现,方便测试和扩展,比如文中所提到扫描数据内容的行数,属于读操作,可以使用io.Reader 接口作为参数,如果是写操作,使用io.Writer 等等,io包中封装了不同接口组合; bufio包中有对应方法接受接口参数进行读写操作;这样数据源只要实现对应读写接口方法即可。

47.忽略defer参数和接收者的计算方式 (重要)

在传递defer 函数参数时,函数的参数是值拷贝传递,传入的值为当前值,比如defer f(a) 在f的定义中,传入参数对a值进行了值拷贝,所以外部a值怎么变,已经和defer函数中的参数值已经没有关系了,如果想继续使用的话, 提供了两种方案:

  1. defer 函数参数采用指针传递,比如f(&a),这样指向同一地址空间,需要函数传入指针类型,这个对于已有封装好的函数是不可行的。
  2. 使用闭包函数,defer func(){f(a)}() 这样可以引用到在return之前a的最终值,如果改成 defer func(a int){f(a)}(a), 则和原来值传递的情况一样。

注意的地方,如果defer的是结构体实例的方法,比如 defer s.f() ,如果s是指针传递,则后续结构体实例s中的成员有变化,也会影响到defer s.f(); 如果采用值传递给接受者s, 则是当前s的值拷贝,后续怎么变都不影响;

概括

  • 应该根据类型、是否必须改变、是否包含无法复制的字段以及对象有多大等因素来决定是使用值还是指针接收器。如有疑问,请使用指针接收器。
  • 使用命名结果参数是提高函数/方法可读性的有效方法,尤其是在多个结果参数具有相同类型的情况下。在某些情况下,这种方法也很方便,因为命名的结果参数被初始化为它们的零值。但要注意潜在的副作用。
  • 返回接口时,注意不要返回一个 nil 指针,而是一个显式的 nil 值。否则,可能会导致意想不到的后果,因为调用者将收到一个非零值。
  • 将函数设计为接收io.Reader类型而不是文件名可以提高函数的可重用性并使测试更容易。
  • 传递指向函数的指针defer和将调用封装在闭包中是克服接收者参数立即求值的两种解决方案。