go阅读基础数据类型,复合数据类型,函数,方法这四章的学习笔记
数据类型
在Go语言中,%取模运算符的符号和被取模数的符号总是一致的,因此-5%3
和-5%-3
结果都是-2。除法运算符/
的行为则依赖于操作数是否全为整数,比如5.0/4.0
的结果是1.25,但是5/4的结果是1,因为整数除法会向着0方向截断余数。
一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的bit位才能正确表示的话,就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的bit位是1的话,那么最终结果可能是负的。
非数NaN则是充满风险的,因为NaN和任何数都是不相等的
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。
常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof
slice
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样;
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
slice的append方法
如下是第一个版本的appendInt
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}
上面当实际的底层数组大小不够时,会直接分配一个2倍大小的空间,这样就避免了经常性开辟空间给这个数组的情况。也可以说是一个空间换时间,让以后的append操作更加的快,这样的场景在很多操作中很常见例如java的arrayList,hashMap等,go中的strings.Builder 里面的grow()
一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:
stack = append(stack, v) // push v
stack的顶部位置对应slice的最后一个元素:
top := stack[len(stack)-1] // top of stack
通过收缩stack可以弹出栈顶的元素
stack = stack[:len(stack)-1] // pop
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。例如
import "sort"
ages := make(map[string]int)
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
结构体中,Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
type Point struct {
X, Y int
}
// 方式一,访问X->Circle.Center.X
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
// 方式二,这是go语言的特性Point,Circle为匿名成员,会将Point和Circle的成员变量直接加入到其中.于是可以这么访问Circle.X
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
个人突然觉得还是不推荐这种写法,在命名上容易有冲突。
匿名函数
如下返回一个匿名函数
// 该匿名函数每次被调用时都会返回下一个数的平方。func squares() func() int { var x int return func() int { x++ return x * x }}func main() { f := squares() fmt.Println(f()) // "1" fmt.Println(f()) // "4" fmt.Println(f()) // "9" fmt.Println(f()) // "16"}
squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。这个变量x生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。
匿名函数中的使用父类函数的变量时,其存储的是变量引用。需要主要其值的变化会直接影响原始数据
defer
你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
像下面的使用会在trace函数进入时调用,但其返回的匿名函数就会等到bigSlowOperation执行完后调用,这样就能记录这个函数的耗时
func bigSlowOperation() { defer trace("bigSlowOperation") // don't forget the extra parentheses log.Printf("aaa") // ...lots of work… time.Sleep(1 * time.Second) // simulate slow operation by sleeping}func trace(msg string) func() { start := time.Now() log.Printf("enter %s", msg) return func() { log.Printf("exit %s (%s)", msg,time.Since(start)) }}
defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值。
在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。
方法
方法声明中将方法的调用者称为方法的接收器(receiver),例如 p.Distance(),中p就是方法的接收器。而 p.Distance的表达式称为选择器,因为他会选择合适的对应p这个对象的Distance方法来执行。
基于指针对象的方法
当调用一个函数时,会对其每一个参数值进行拷贝。当你需要更新其成员时就需要用到指针对象。
在程序中有一个约定,如果一类中有一个指针作为接收器的方法,则所有方法都必须有一个指针接收器,即使是那些不需要指针接收器的方法。
**对于类方法的调用。不管你是method的received是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的。编译器会帮你自动转型。**这点挺好的,一开始看着指针和非指针都是一样的调用,还没弄清楚为啥
nil也是一个合法的接收器类型。
通过嵌入结构体来拓展类型的方法,觉得和面向对象中的组合一致,增加些许语法糖。
方法值和方法表达式
p.Distance()形式的方法调用,实际上是2步。
- p.Distance叫做选择器,编译器会返回一个方法"值"->一个将方法Distance绑定到特定接收器变量的函数,这个函数可以不指定其接收器即可被调用
- 随后
()
就是对上一个函数进行调用