Go语言核心编程第7章 “语言陷阱”

学习目标:

Go语言核心编程第7章 “语言陷阱”

学习内容:

Go语言核心编程第7章 “语言陷阱”

第7章 语言陷阱

7.1 多值赋值和短变量声明

Go 语言支持多值赋值,在函数或方法内部也支持短变量声明并赋值,同时 Go 语言依据类型字面量的值能够自动进行类型推断。

7.1.1 多值赋值

可以一次性声明多个变量,并可以在声明时赋值,而且可以省略类型,但必须遵守一定的规则要求,具体看下面的示例。如下都是合法的:

相同类型的变量可以在末尾带上类型
var x, y int
var x, y int = 1, 2

如果不带类型编译器,则可以直接进行类型推断
var x, y = 1, 2
var x, y = 1, "tata"

不同类型的变量声明和隐式初始化可以使用如下语法
var (
	x int
	y string
)

如下都是不合法的:

多赋值语句中每个变量后面不能都带上类型
var x int, y int = 1, 2
var x int, y int
var x int, y string
var x int, y string = 1, "tata"

多值赋值的两种格式
( 1 ) 右边是一个返回多值得表达式,可以是返回多值的函数调用,也可以是 range 对 map 、slice 等函数的操作,还可以是类型断言。例如:

函数调用
x, y = f()

range表达式
for k,v := range map {
} 

类型断言
v,ok := i.(xxx)

( 2 ) 赋值的左边操作数和右边的单一返回值的表达式的个数一样,逐个从左向右依次对左边的操作数赋值。例如:

x,y,z = a,b,c

多值赋值语义
多值赋值看似简化了代码,但相互引用会产生让人困惑结果。关键是要理解多值赋值的语义,才能消除这种困惑。多值赋值包括两层语义:
( 1 ) 对左侧操作数中的表达式、索引值进行计算和确定,首先确定左侧的操作数的地址;
然后对右侧的赋值表达式进行计算,如果发现右侧的表达式计算引用了左侧的变量,则创建临时变量进行值拷贝,最后完成计算。
( 2 ) 从左到右的顺序依次赋值。

package main
import "fmt"
func main() {
	x := []int{1, 2, 3}
	i := 0
	i, x[i] = 1, 2 //set i = 1, x[0] = 2
	fmt.Println(i, x)// 1 [2 2 3]
	
	x = []int{1, 2, 3}
	i = 0
	x[i], i = 2, 1 // set x[0] = 2, i = 1
	fmt.Println(i, x) // 1 [2 2 3]

	x = []int{1, 2, 3}
	i = 0
	x[i], i = 2, x[i] 
	fmt.Println(i, x) // 1 [2 2 3]

	x[0], x[0] = 1, 2
	fmt.Println(x[0]) //2
}
7.1.2 短变量的声明和赋值

短变量的声明和赋值是指在 Go 函数或类型方法内部使用 " := " 声明并初始化变量,支持多值赋值,格式如下:

a:= va
a, b := va, vb

短变量的声明和赋值的语法要约:
( 1 ) 使用 " := " 操作符,变量的定义和赋值同时完成。
( 2 ) 变量名后不要跟任何类型名,Go 编译器完成靠右边值进行推导。
( 3 ) 支持多值短变量声明赋值。
( 4 ) 只能用在函数和类型方法的内部。
短变量的声明和赋值中的最容易产生歧义的是多值变量的声明和赋值,这个问题的根源是Go 语言的语法允许多值短变量声明和赋值的多个变量中,只要有一个是新变量就可以使用” := “进行赋值。也就是说,在多值短变量的声明和赋值中,至少有一个变量是新创建的局部变量,其他的变量可以复用以前的变量,不是新创建的变量执行的仅仅是赋值。看一个具体示例:

package main
var n int
func foo() (int error) {
	return 1, nil
}
//访问全局变量n
func g() {
	println(n)
}
func mian() {
	//此时 main 函数作用域里面没有 n
	//所以创建新的局部变量 n
	n, _ := foo()
	//访问的是全局变量 n
	g()
	//访问的是main函数作用域下的n
	println(n) //1
}

a,b := va,vb 什么时候定义新变量,什么时候复用已存在变量有以下规则:
( 1 ) 如果编译想通过,则 a 和 b 中至少要有一个是新定义的局部变量,不能同时预先声明a 和 b 两个局部变量。
( 2 ) 如果赋值语句 a,b := va,vb 所在的代码块中已经存在一个局部变量 a,则赋值语句a,b := va,vb 不会创建新变量 a,而是直接使用 va 赋值给已经声明的局部变量a ,但是会创建新变量 b,并将 vb 赋值给 b 。
( 3 ) 如果在赋值语句 a,b := va,vb 所在的代码块中没有局部变量 a 和 b,但在全局命名空间有变量 a 和 b,则该语句会创建新的局部变量,并使用 va 和 vb 初始化它们,此时赋值语句所在的局部作用域类内,全局的 a 和 b 被屏蔽。
赋值操作符 " = " 和 ” := “ 的区别:
( 1 ) ” = “ 不会声明并创建新变量,而是在当前赋值语句所在的作用域从内往外逐层区搜寻变量,如果没有搜索到相同变量名,则报编译错误。
( 2 )" := ” 必须出现在函数或类型方法内部。
( 3 )“ := ” 至少要创建一个局部变量并初始化。

func f() {
	var a, b int
	a, b := 1, 2 //无法通过编译 
}
func f() {
	var a int
	a, b := 1, 2 //可以通过编译 
}
func f() {
	a, b := 1, 2 //可以通过编译 
}

如何避免" := "引入的副作用?一个好的办法就是先声明变量,然后使用 “ = ” 赋值。例如:

func f() {
	var a,b int
	a, b = 1,2
}

多值短变量声明赋值 “ :=” 的最佳使用场景是在错误处理上,例如:

a, err := f()
if err != nil {
	xxx
}
//此时 err 可以是已经存在的 err 变量,只是重新赋值了
b, err := g()

7.2 range 复用临时变量

先看一段简单的代码:

package main
import "sync"
func main() {
	wg := sync.WaitGroup{}
	si := []int{1,2,3,4,5,6,7,8,9,10}
	for i := range si {
		wg.Add(1)
		go func() {
			println(i)
			wg.Done()
		}
	}
	wg.Wait()
}
运行结果:
9
9
9
9
9
9
9
9
9
9

程序结果并没有按照预期一样遍历切片,而是全部打印 9,有两点原因
( 1 ) for range 下面的迭代变量 i 的值是共用的。
( 2 ) main 函数所在的 goroutine 和 后续启动的 goroutines 存在竞争关系。
上述示例证实了 range 共享临时变量。range 在迭代写的过程中,多个 goroutine 并发地去读。正确的写法是使用函数参数做一次数据复制,而不是闭包。示例如下:

package main
import "sync"
func main() {
	wg := sync.WaitGroup{}
	si := []int{1,2,3,4,5,6,7,8,9,10}
	for i := range si {
		wg.Add(1)
		go func(a int) {
			println(a)
			wg.Done()
		}(i)
	}
	wg.Wait()
}
运行结果
9
0
1
2
3
4
5
6
7
8

可以看到新程序的运行结果符合预期。这个不能说是缺陷,而是 Go 语言设计者为了性能
而选择的一种设计方案,因为大多情况下 for 循环块里的代码是在同一个 goroutine 里运行的,为了避免空间的浪费和 GC 的压力,复用了 range 迭代临时变量。语言使用者明白这个规约,在for 循环下调用并发时要复制迭代变量后再使用,不要直接引用 for 迭代变量。

7.3 defer 陷阱

defer的副作用。第一个副作用是对返回值的影响,第二个副作用是对性能的影响。
defer 和函数返回值
defer 中如果引用了函数的返回值,则因引用形式不同会导致不同的结果,这些结果往往给初学者造成很大的困惑,我们先来看一下如下三个函数的执行结果:

package main
func f1() (r int) {
	defer func() {
		r++
	}()
	return 0
}

func f2() (r int) {
	t := 5
	defer func() {
		t = t + 5
	}()
	return t
}

func f3() (r int) {
	defer func(r int) {
		r = r + 5
	}(r)
	return 1
}

func main() {
	println("f1=", f1()) //1
	println("f2=", f2()) //5
	println("f3=", f3()) //1
}

f1、f2、f3都是带命名返回值的函数,返回值都是变量 r
对于函数而言:
( 1 ) 函数调用方负责开辟栈空间,包括形参和返回值的空间。
( 2 ) 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值。
分析 f1
( 1 ) r 是函数的有名返回值,分配在栈上,其地址又被称为返回值所在的栈区。首先 r 被初始化为 0。
( 2 ) “ return 0 ” 会复制 0 到返回值栈区,返回值 r 被赋值为 0。
( 3 ) 执行 defer 语句,由于匿名函数对返回值 r 是闭包引用,所以 r++ 执行后,函数返回值被修改为 1。
( 4 ) defer 语句执行完后 RET 返回,此时函数的返回值仍然为 1。
分析 f2
( 1 ) 返回值 r 被初始化为 0。
( 2 ) 引入局部变量 t 初始化为 5。
( 3 ) 复制 t 的值 5 到返回值 r 所在的栈区。
( 4 ) defer 语句后面的匿名函数是对局部变量 t 的闭包引用,t 的值被设置为 10。
( 5 ) 函数返回,此时函数返回值栈区上的值仍然是 5。
分析 f3
( 1 ) 返回值 r 被初始化为 0。
( 2 ) 复制 1 到返回值 r 所在的栈区。
( 3 ) 执行 defer,defer 后匿名函数使用的是传参数调用,在注册 defer 函数时将函数返回值 r 作为实参传进去,由于函数调用是值拷贝,所以 defer 函数执行后只是把形参值变为5,对实参没有任何影响。
( 4 ) 函数返回,此时函数返回值栈区上的值仍然是 1。
综上,对于带 defer 函数返回整体上有三个步骤
( 1 ) 执行 return 的值拷贝,将 return 语句返回的值复制到函数返回值栈区。
( 2 ) 执行 defer 语句。
( 3 ) 执行 调整 RET 指令。
在 defer 中修改函数返回值不是一种明智的编程方法,在实际编程中应尽可能避免此种情况。还有一种彻底解决该问题的方法是,在定义函数时,使用不带返回值名的格式。通过这种方式,defer 就不能直接引用返回值的栈区,也就避免了返回值被修改的问题,看一下下面的代码。

package main
func f4() int {
	r := 0
	defer func() {
		r++
	}
	return r
}
func f5() int {
	r := 0
	defer func(i int) {
		i++
	}(r)
	return 0
}
func main() {
	println("f4=", f4()) //0
	println("f5=", f5()) //0
}

从 f3、 f4 的执行结果可以看出,不管 defer 怎么操作,都不会改变函数的 return 值,这是一种好的编程模式。

7.4 切片困惑

数组是切片实现的基础

7.4.1 数组

Go 的数组是有固定个相同类型元素的数据结构,底层采用连续的内存空间存放,数组一旦声明后大小就不可改变了。
注意:Go 中的数组是一个基本类型,数组的类型不仅包括其元素类型,也包括大小,[2]int 和 [5]int 是两个完全不同的数组类型。
创建数组
( 1 ) 声明时通过字面量进行初始化。
( 2 ) 直接声明,不显式地进行初始化。

package main
import "fmt"
func main() {
	//指定大小的显式初始化
	a := [3]int{1,2,3}
	
	//通过"..."由后面的元素个数推断数组大小
	b := [...]int{1,2,3}
	
	//指定大小,并通过索引值初始化,未显式初始化的元素被置为"零值"
	c := [3]int{1: 1 , 2: 3}
	
	//指定大小但不显式初始化,数组元素全被置为"零值"
	var d [3]int
	fmt.Printf("len=%d,value=%v\n", len(a), a) //len=3,value=[1 2 3]
	fmt.Printf("len=%d,value=%v\n", len(b), b) //len=3,value=[1 2 3]
	fmt.Printf("len=%d,value=%v\n", len(c), c) //len=3,value=[0 1 3]
	fmt.Printf("len=%d,value=%v\n", len(d), d) //len=3,value=[0 0 0]
}

数组名无论作为函数实参 ,还是作为 struct 嵌入字段,或者数组之间的直接赋值,都是值拷贝,不像 C 语言数组名因场景不同,可能是值拷贝,也可能是指针传递。 Go 语言的数组不存在这种歧义,数组的一切传递都是值拷贝,体现在以下三个方面:
( 1 ) 数组间的直接赋值。
( 2 ) 数组作为函数参数。
( 3 ) 数组内嵌到 struct 中。

下面以一个示例来证明这三条:

package main
import "fmt"
func f(a [3]int) {
	a[2] = 10
	fmt.Printf("%p,%v\n", &a, a)
}
func main() {
	a := [3]int{1, 2, 3}
	//直接赋值是值拷贝
	b := a
	//修改 a 元素的值并不影响 b
	a[2] = 4
	fmt.Printf("%p,%v\n", &a, a) // 0xc420012220,[1 2 4] 
	fmt.Printf("%p,%v\n", &b, b) // 0xc420012240,[1 2 3] 
	//数纽作为函数参数仍然是值拷贝
	f(a) // 0xc4200122c0, [1 2 10]
	c := struct { 
		s [3]int 
	}{ 
		s: a ,
	} 
	//结构是值拷贝,内部的数组也是值拷
	d := c 
	//修改 c 中的数纽元素值并不影响 a
	c.s[2] = 30 
	//修改 d 中的数纽元素值并不影响 c
	d.s[2] = 20 
	fmt.Printf("%p,%v\n", &a, a) // 0xc420012220 ,[1 2 4]
	fmt.Printf("%p,%v\n", &b, b) // 0xc420012300 ,{[1 2 30]}
	fmt.Printf("%p,%v\n", &c, c) // 0xc420012320 ,{[1 2 20]}
}

由于数组的大小一旦声明后就不可修改,所以实际使用场景并不广泛,下面介绍使用广泛的数组切片。

7.4.2 切片

切片创建
( 1 ) 通过数组创建
array [b : e] 创建一个包括 e-b 个元素的切片,第一个元素是 array [b],最后一个元素是 array [e-1]。
( 2 ) make
通过内置的 make 函数创建, make( []T,len,cap)中的 T 是切片元素类型,len 是长度, cap是底层数组的容量,cap 是可选参数。
( 3 ) 直接声明
可以直接声明一个切片,也可以在声明切片的过程中使用字面量进行初始化, 直接声明但
不进行初始化的切片其值为 nil。例如:

var a []int //a is nil
var a []int = []int{1,2,3,4}

切片数据结构
通常我们说切片是一种类似的引用类型,原因是其存放数据的数组是通过指针间接引用的
所以切片名作为函数参数和指针传递是一样的效果。切片的底层数据结构如下:

//切片的底层数据结构
//src/runtime/slice.go 
type slice struct { 
	array unsafe.Pointer 
	len int 
	cap int
}

可以看的切片的数据结构有三个成员,分别是指向底层数组的指针、切片的当前大小和底
层数组的大小 。当 len 增长超过 cap 时,会申请一个更大容量的底层数组,并将数据从老数组复制到新申请的数组中。
nil 和空切片
make([]int,0) 和var a []int 创建的切片是有区别的。前者的切片指针有分配,后者的内部指
针为0。示例如下:

package main
import (
	"fmt"
	"reflect"
	"unsafe"
)
func main() {
	var a[] int
	b := make([]int, 0)
	if a == nil {
		fmt.Println("a is nil")
	} else {
		fmt.Println("a is not nil")
	}
	//虽然 b 底层数组大小为 0,但切片并不是 nil
	if b == nil {
		fmt.Println("b is nil")
	} else {
		fmt.Println("b is not nil")
	}
	//使用反射中的 SliceHeader 来获取切片运行时的数据结构
	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	fmt.Printf("len=%d,cap=%d,type=%d\n",len(a), cap(a), as.Data)
	fmt.Printf("len=%d,cap=%d,type=%d\n",len(b), cap(b), bs.Data)
}
运行结果
a is nil
b is not nil
len=0,cap=0,type=0
len=0,cap=0,type=5537704

可以看出 var a []int 创建的切片是一个 nil 切片 (底层数组没有分配,指针指向 nil )
可以看出 make([]int,0) 创建的切片是一个空切片 (底层数组指针非空,但是底层数组是空的 )
看一下 makeslice底层代码实现:

func makeslice(et *_type, len, cap int) slice {
	maxElements := maxSliceCap(et.size)
	if len < 0 || uintptr(len) > maxElements {
		panic(errorString("makeslice: len out of range"))
	}
	if cap < len || uintptr(cap) > maxElements {
		panic(errorString("makeslice: cap out of range"))
	}
	//调用 mallocgc 分配空间
	p := mallocgc(et.size*unitptr(cap), et, true)
	return slice(p, len, cap)
}

接下来看一下 len 和 cap 是 0 的情况下,mallocgc 的代码片段:

//可以看到如果 len 和 cap 是 0 的情况下,直接返回一个固定的 zerobase 全局变量地址
func mallocgc(size uintptr, type *_type, needzero bool) unsafe.Pointer {
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
	...
}
var zerobase uintptr

多个切片引用同一个底层数组引发的混乱
切片可以由数组创建,一个底层数组可以创建多个切片,这些切片共享底层数组,使用
append 扩展切片过程中可能修改底层数组的元素,间接地影响其他切片的值,也可能发生数组复制重建,共用底层数组的切片,由于其行为不明朗,不推荐使用。看一个示例:

package main
import (
	"fmt"
	"reflect"
	"unsafe"
)
func main() {
	a := []int{0 , 1 , 2 , 3 , 4 , 5 , 6) 
	b := a[0 : 4] 
	as : = (*reflect.SliceHeader)(unsafe.Pointer(&a)) 
	bs : = (*reflect.SliceHeader)(unsafe.Pointer(&b)) 
	// 共享底层数纽
	fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n",len(a),cap(a),as.Data) 
	fmt.Printf("b=%v,len=%d,cap=%d,type=%d\n",len(b),cap(b),bs.Data) 
	b = append(b , 10 , 11 , 12)
	//a、b 继续共享底层数组,修改 b 会影响共享底层的数组,间接影响 a
	fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n",len(a),cap(a)) 
	fmt.Printf("b=%v,len=%d,cap=%d,type=%d\n",len(b),cap(b)) 
	//len(b)=7, 底层数纽容量是7, 此时需要重新分配数组,并将原来数组复制到新数组
	b = append(b , 13 , 14) 
	as : = (*reflect.SliceHeader)(unsafe.Pointer(&a)) 
	bs : = (*reflect.SliceHeader)(unsafe.Pointer(&b)) 
	//可以看到 a 和 b 指向底层数组的指针已经不同了
	fmt.Printf("a=%v,len=%d,cap=%d,type=%d\n",len(a),cap(a),as.Data) 
	fmt.Printf("b=%v,len=%d,cap=%d,type=%d\n",len(b),cap(b),bs.Data) 
}
程序运行结果:
a=[0 1 2 3 4 5 6], len=7, cap=7, type=842350575680 
b=[0 1 2 3], len=4, cap=7, type=842350575680 
a=[0 1 2 3 10 11 12], len=7, cap=7 
b=[0 1 2 3 10 11 12], len=7, cap=7 
a=[0 1 2 3 10 11 12], len=7, cap=7 , type=842350575680 
b=[0 1 2 3 10 11 12 13 14], len=9, cap=l4, type=842350788720

问题总结:多个切片共享一个底层数据,其中一个切片的 append 操作可能引发如下两种
情况。
( 1) append 追加的元素没有超过底层数组的容量,此种 append 操作会直接操作共享的底层数组,如果其他切片有引用数组被覆盖的元素,则会导致其他切片的值也隐式地发生变化。
(2) append 追加的元素加上原来的元素如果超出底层数组的容量 ,则此种 append 操作会重新申请新数组,并将原来数组值复制到新数组。

由于有这种二义性,所以在使用切片的过程中应该尽量避免多个切片共享底层数组, 可以
使用 copy 进行显式的复制。

7.5 值,指针和引用

7.5.1 传值还是传引用

Go 只有一种函数传递规则,值拷贝
( 1 )函数参数传递时使用的是值拷贝。
( 2 )实例赋值给接口变量,接口对实例的引用是值拷贝。
有时在明明是值拷贝的地方,结果却修改了变量的内容,有以下两种情况:
( 1 )直接传递的是指针。指针传递同样是值拷贝,但指针和指针副本的值指向的地址是同一个地方,所以能修改实参值。
( 2 )参数是复合数据类型,这些复合数据类型内部有指针类型的元素,此时参数的值拷贝并不影响指针的指向。
Go 复合类型中 chan map slice interface 内部都是通过指针指向具体的数据,这些类型的变量在作为函数参数传递时,实际上相当于指针的副本 。下面看一下 runtime 里面的具体定义。
chan 的底层数据结构如下:

type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

从 chan 在 runtime 里面的数据结构可知,通道元素的存放地址由 buf 指针确定,chan 内部的数据也是间接通过指针访问的。
map 的底层数据结构如下:

type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
	extra *mapextra // optional fields
}

从 map 在 runtime 里面的数据结构同样可以清楚地看到,其通过 buckets 指针来间接引用 map 中的存储结构。
slice 的底层数据结构如下:

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

slice 一样用 unitptr 指针指向底层存放数据的数组。
interface 的底层数据结构如下:

type nonEmptyInterface struct {
	// see ../runtime/iface.go:/Itab
	itab *struct {
		ityp *rtype // static interface type
		typ  *rtype // dynamic concrete type
		hash uint32 // copy of typ.hash
		_    [4]byte
		fun  [100000]unsafe.Pointer // method table
	}
	word unsafe.Pointer
}
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}

接口内部通过一个指针指向实例值或地址的副本

7.5.2 函数名的意义

Go 的函数名和匿名函数字面量的值有 3 层含义:
( 1 ) 类型信息,表明其数据类型是函数类型。
( 2 ) 函数名代表函数的执行代码的起始位置。
( 3 ) 可以通过函数名进行函数调用,函数调用格式为 func_name(param_list)。在底层执行方面包含以下 4 部分内容。

  • 准备好参数
  • 修改 PC 值,跳转到函数代码起始位置开始执行
  • 复制值到函数的返回值栈区
  • 通过 RET 返回函数调用的下一条指令处继续执行
7.5.3 引用语义

C++里面的引用的含义就是别名,Go 语义规范中并没有引用的概念,但为了论述方便,闭包对外部变量的引用,可认为是建立了一个和外部变量同名的"引用",该引用和外部变量指向相同的地址。还有一种就是就是 Go 语言针对闭包,显式地扩大了形参的可见域,使其在函数返回的闭包中仍然可见。这两种本质都是同一件事情,就是闭包可以访问和改变外部环境中的变量。

package main
func fa(a int) func(i int) int {
	return func(i int) int {
		println(&a, a)
		a = a + i
		return a
	}
}
func main() {
	//f 是一个闭包,包括对函数 fa 形式参数 a 的"同名引用"
	f := fa(1)
	println(f(1)) //2
	println(f(2)) //4
}
运行结果:
0xc4200140b8 1
2
0xc4200140b8 2
4

7.6 习惯用法

有些是强制的,有些是非强制的。

7.6.1 干净与强迫症

Go 在代码干净上有近乎苛刻的要求
( 1 ) 编译器不能通过未使用的局部变量 ( 包括未使用的标签 )
( 2 ) "import"未使用的包也同样无法通过编译
( 3 ) 所有的控制结构、函数和方法定义的“ { ” 放到行尾,而不能另起一行
( 4 ) 提供 go fmt 工具格式化代码,使所有的代码风格保持统一

7.6.2 comma,ok表达式

常见的几个 comma,ok 表达式如下:
( 1 ) 获取 map 值
获取 map 中不存在键的值不会发生异常,而是会返回值类型的零值,如果想确定 map 中是否存在 key ,则可以使用获得 map 值的 comma,ok 语法。示例如下:

m := make(map[string]string)
v, ok := m["some"]
if !ok {
	println("m[some] is nil")
} else {
	println("m[some] =", v)
}

( 2 ) 读取 chan 值
读取已经关闭的通道,不会阻塞,也不会发生 panic ,而是一直返回该通道的零值。怎么判断通道是否已经关闭?
有两种方法,一种就是读取通道的 comma,ok 表达式,另一种就是通过 range 循环迭代。示例如下:

c := make(chan int)
go func() {
	c <- 1
	c <- 2
	close(c)
}()
for {
	v, ok := <-c
	if ok {
		println(v)
	} else {
		break
	}
}
//使用 range 更加简介
for v := range c{
	println(v)
}

( 3 ) 类型断言
接口的类型断言可以使用 comma,ok 语句来确定接口是否绑定某个实例类型,或者判断接口绑定的实例类型是否实现另一个接口。实例如下:

判断接口 body 绑定的实例是否实现了另一个接口类型 io.ReadCloser
rc, ok := body.(io.ReadCloser)
7.6.3 简写模式

Go 语言很多重复的引用或声明可以用 “()” 进行简写
( 1 ) import 多个包。例如:

//推荐写法
import (
	"bufio"
	"bytes"
)
//不推荐写法
import "bufio"
import "bytes"

( 2 ) 多个变量声明。例如:

//推荐写法
var (
	x string
	y string
)
//不推荐写法
var x string
var y string
7.6.4 包中的函数或方法设计

很多包的开发者会在内部实现两个“同名”的函数或方法,一个首字母大写,用于导出 API 供外部调用;一个首字母小写,用于实现具体逻辑。一般首字母大写的函数调用首字母小写的函数,同时包装一些功能;首字母小写的函数复杂更多的底层细节。
大部分情况下我们不需要两个同名且只是首字母大小写不同的函数,只有在函数逻辑很复杂,而且函数在包的内、外部都被调用的情况下,才考虑拆分成为两个函数进行实现。一方面减少单个函数的复杂性,另一方面隔离调用。

7.6.5 多值返回函数

多值返回函数里如果有 error 或 bool 类型的返回值,则应该将 error 或 bool 作为最后一个返回值。这是一种编程习惯。Go 标准库的写法也遵循这样的规则。

你可能感兴趣的:(golang,开发语言,后端)