golang系列(2)-指针及内存分配,map,函数与作用域及闭包,panic,fmt输入与精度

marvin

指针
go 中的指针比较简单, 因为它不允许进行指针操作, 你不能通过指针做一些危险的事情, 其有两个用法:
- & 取地址
- (*) 根据地址取值
例如:
n := 18
p := &n
fmt.Println(p)
fmt.Printf("%T\n", p)
这里定义的 p 保存的是 int 类型的地址 , 表示为 *int, 里面的值是一个16进制的数
new 和 make
为了说明先来看一个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["h"] = 100
fmt.Println(b)
}
执行上面的代码会引发 panic, 为什么呢, 在 go 语言中对于引用类型的变量, 我们在使用的时候不仅要声明它, 还要为它分配内存空间, 否则我们的值就没办法存储. 而对于值类型的声明不需要分配内存空间, 是因为它们在声明的时候已经默认分配好了内存空间. 要分配内存就引出来今天的 new 和 make. 这里的 new 和 make 是内建的两个函数, 主要用来分配内存.
new
new 是一个内置的函数, 它的函数签名如下:
func new(类型) *类型
new 函数可以返回一个特定类型的类型地址
make
make 也是用于分配内存地址, 区别于 new, 它只用于 slice, map 以及channel的内存创建, 而且它返回的类型就是这三个类型本身, 而不是他们的指针类型, 因为这三种类型就是引用类型, make 函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make 函数是无可替代的, 我们在使用 slice, map 以及 channel的时候, 都需要使用 make 进行初始化, 然后才可以对它们进行操作
map
map 是一种无序的基于 key-value 的数据结构, Go 语言中的 map 是引用类型, 必须初始化才能使用
map 的定义
map 的定义如下:
map[key的类型]值的类型
map类型的变量默认初始值为 nil, 需要使用 make 函数来分配内存, 语法如下:
make(map[key的类型]值的类型, [cap])
这里的 cap 表示 map 的容量, 这个参数虽然不是必须的, 但是我们应该在初始化 map 的时候就为其指定一个合适的容量, 在取map的值的时候, 会分别返回value, ok, 这里 ok 表示值是否存在
value, ok := map1["hhh"]
map 元素的删除
可以使用内建的 delete 函数将元素从 map 映射中删除, 若 map 为 nil 或没有此元素, delete 不进行操作, 语法如下:
func delete(对应的map元素, 要删除的key)
函数
函数分为匿名函数和闭包, 而且函数在 go 中属于一等公民, 也就是说函数支持传递
函数定义
func 函数名称(参数)(返回值) {
函数体
}
这里有几个规则
- 在同一个包里面, 函数名称不能重复
- 参数, 参数由参数变量和参数变量的类型组成, 多个参数之间使用 , 分隔
- 返回值: 返回值由返回值变量和其变量类型组成, 也可以只写返回值的类型, 多个返回值必须用 () 包裹, 并用, 分隔
- 函数体: 实现指定功能的代码块
下面是一个多个返回值的例子:
func f()(int, string) {
return 1, "hello"
}
当几个连续的参数类型一样的时候, 可以进行简写, 例如:
func f1(x, y int) int {
return x + y
}
可变长参数
当参数值不固定的时候可以使用可变值参数, 下面是一个可变长度参数的例子:
func f3(x int, y ...int) {
fmt.Println(x)
fmt.Println(y)
}
这里的 y 是一个切片类型, 其中是可变参数的值. 需要注意的是可变值参数必须放在参数列表的最后. 另外 go 语言中不能指定默认参数
命名返回值
如果在声明返回值的时候使用了名称, 则在函数中可以直接使用返回值变量, 同时 return 后面也可以省略返回值变量, 下面是一个命名返回值的例子:
func f6(x, y int)(sum int) {
sum = x + y
return
}
defer 语句
go 语言中的 defer 语句会将其后面跟随的语句进行延迟处理. 在 defer 归属的函数即将返回时, 将延迟处理的语句按 defer 定义的逆序进行执行, 下面时 defer 语句的一个例子:
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出结果为: start, end, 3, 2, 1. 由于 defer 语句延迟调用的特性, 所以 defer 语句能非常方便地处理资源释放问题. 比如资源清理, 文件关闭, 解锁以及记录时间等
defer 的执行时机
在 go 语言中 return 语句在底层并不是原子操作, 它分为给返回值赋值和RET指令两步. 而 defer 语句执行的时机就在返回值赋值操作后, return 指令执行前.
函数作为返回值或参数
函数作为一等公民, 可以作为参数传递, 在指定函数类型的时候不需要指定函数体, 下面是一个例子:
func ff(x func() int) {
fmt.Println(x())
}
因为这里的 x 是没有参数的函数类型, 因此可以直接调用. 下面来看一个函数作为返回值的例子:
func ff(a, b int) int {
return a + b
}
func f8(x func() int) func (int, int) int {
return ff
}
这里将 ff 作为 f8 的返回值, 返回值的类型刚好匹配两个整型参数和一个整型返回值
内置函数
go 本身提供了一些内置函数供我们使用, 主要有以下几个, 其中有一些是已经见过的:
- close 主要用来关闭 channel
- len 用来求长度, 比如 string, array, slice, map, channel
- new 用来分配内存, 主要用来分配值类型, 比如 int, struct, 返回的是指针
- make 用来分配内存, 主要分配引用类型, 比如 channel, map, slice
- append 用来追加元素到数组, slice中
- panic 和 recover 用来做错误处理
go 语言中目前没有异常机制, 可以使用 panic / recover 模式来处理错误, panic 可以在任何地方引发, 但 recover 只有在 defer 调用的函数中有效. 为了理解下面来看一个例子:
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
这里的输出为:
func A
panic: panic in B
// ...
这里通过 panic 在函数B中引起了一个崩溃, 以后的逻辑都不会执行. 这个时候我们可以通过 recover 将程序恢复回来, 继续执行:
func funcB() {
defer func() {
err := recover()
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
这里在 B要退出的时候我们通过 recover 判断是否有 panic, 然后执行恢复. 需要注意的是, defer 一定要在可能引发 panic 的语句之前定义.
变量作用域
全局变量
全局变量时定义在函数外部的变量, 它在程序整个运行周期内部有效, 在函数中可以访问到全局变量, 例如:
var num int64 = 10
局部变量
局部变量又分为两种:
- 函数作用域
- 语句块作用域
函数中定义的变量只能在函数内使用 , 语句块中的变量也只能在语句块中使用
闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体, 简单来说, 闭包等于函数加引用环境, 为了说明来看一个例子:
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30)) // 60
f1 := adder()
fmt.Println(f1(40))
fmt.Println(f1(50)) // 90
}
变量 f 时一个函数并且它能够引用其外部作用域中的变量 x, 因此 f 就是一个闭包. 在闭包的生命周期内, 引用环境一直有效
fmt的宽度标识符
宽度通过一个紧跟在百分号后面的十进制数指定, 如果未指定宽度, 则表示值时除必填之外不做填充. 精度通过宽度后的点数指定. 如果未指定精度, 会使用默认精度, 如果点数没有跟精度, 则表示精度为 0. 例如:
fmt.Printf("%9.2f\n", n)
这里的9表示宽度为9, 2表示精度为2. 这里输出的结果包括小数点在内只能是9位. 当没有指定精度而宽度较大, 则用精度来补足, 否则在前面添加空白. 默认是右对齐, 可以通过修改对齐方式让空白出现在后面:
fmt.Printf("%-5s", "HEL")
这里会直接输出 "HEL ", 补足宽度的空白出现在后面
fmt 获取输入
go 提供了3个用于从标准输入中获取用户的输入, 分别为:
- Scan(a ..interface{})(n int, err error)
- Scanf(format stirng, a ...interfaces{})(n int, err error)
- SCanIn
Scan 从标准输入扫描文本, 读取由空白符分割的值保存到传递给本函数的参数中, 换行符也视为空白符, 同时本函数返回成功扫描的数据个数和遇到的任何错误, 如果读取的数据个数比提供的参数少, 会返回一个错误报告原因.
下面是一个使用 Scan 的例子:
func main() {
var (
name string
age int
married bool
)
fmt.Scan(&name, &age, &married)
fmt.Printf("扫描结果 name: %s, age: %d, married: %t \n", name, age, married)
}
运行后在终端依次输入 "hello 10 false" 注意保留空格, 即可看到输出为: 扫描结果 name: hello, age: 10, married: false.
Scanf 也是从标准输入扫描文本, 根据 format 指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中, 同时也返回成功扫描的数据个数和遇到的错误.
ScanIn 类似Scan, 它遇到换行时才停止扫描, 最后一个数据后面必须有换行或达到结束位置, 返回值与其他两个一样