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

marvin

marvin

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

指针

go 中的指针比较简单, 因为它不允许进行指针操作, 你不能通过指针做一些危险的事情, 其有两个用法:

  1. & 取地址
  2. (*) 根据地址取值

例如:

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 函数名称(参数)(返回值) {
	函数体
}

这里有几个规则

  1. 在同一个包里面, 函数名称不能重复
  2. 参数, 参数由参数变量和参数变量的类型组成, 多个参数之间使用 , 分隔
  3. 返回值: 返回值由返回值变量和其变量类型组成, 也可以只写返回值的类型, 多个返回值必须用 () 包裹, 并用, 分隔
  4. 函数体: 实现指定功能的代码块

下面是一个多个返回值的例子:

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 本身提供了一些内置函数供我们使用, 主要有以下几个, 其中有一些是已经见过的:

  1. close 主要用来关闭 channel
  2. len 用来求长度, 比如 string, array, slice, map, channel
  3. new 用来分配内存, 主要用来分配值类型, 比如 int, struct, 返回的是指针
  4. make 用来分配内存, 主要分配引用类型, 比如 channel, map, slice
  5. append 用来追加元素到数组, slice中
  6. 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

局部变量

局部变量又分为两种:

  1. 函数作用域
  2. 语句块作用域

函数中定义的变量只能在函数内使用 , 语句块中的变量也只能在语句块中使用

闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体, 简单来说, 闭包等于函数加引用环境, 为了说明来看一个例子:

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个用于从标准输入中获取用户的输入, 分别为:

  1. Scan(a ..interface{})(n int, err error)
  2. Scanf(format stirng, a ...interfaces{})(n int, err error)
  3. 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, 它遇到换行时才停止扫描, 最后一个数据后面必须有换行或达到结束位置, 返回值与其他两个一样

指针
new 和 make
new
make
map
map 的定义
map 元素的删除
函数
函数定义
可变长参数
命名返回值
defer 语句
defer 的执行时机
函数作为返回值或参数
内置函数
变量作用域
全局变量
局部变量
闭包
fmt的宽度标识符
fmt 获取输入