Go 基础之指针、map、函数、文件操作、结构体(六)
指针
任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话是要加载到内存(假设内存地址0x123456),我在程序中把这段话赋值给变量A
,把内存地址赋值给变量B
。这时候变量B
就是一个指针变量。通过变量A
和变量B
都能找到我的座右铭。
Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行“取地址”操作。Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int
、*int64
、*string
等。
取变量指针的语法如下:
ptr := &v // v的类型为T
其中:
- v:代表被取地址的变量,类型为
T
- ptr:用于接收地址的变量,ptr的类型就为
*T
,称做T的指针类型。*代表指针。
举个例子:
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
我们来看一下b := &a
的图示:
指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
输出如下:
type of b:*int
type of c:int
value of c:10
总结: 取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
指针传值示例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
new和make
我们先来看一个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["沙河娜扎"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。
new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中,
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
本节开始的示例代码中var a *int
只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}
make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。
本节开始的示例中var b map[string]int
只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["沙河娜扎"] = 100
fmt.Println(b)
}
new与make的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
创建slice
make([]Type, len, cap)
cap可以省略。当cap省略时,默认等于len。此外cap >= len >= 0的条件必须成立。
package main
import "fmt"
func main() {
demo := make([]int, 10)
fmt.Println("demo:", demo)
}
创建map
make(map[keyType] valueType, size)
keyType表示map的key类型,valueType表示map的value类型。size是一个整型参数,表示map的存储能力,该参数可省略。
package main
import "fmt"
func main() {
demo := make(map[string]int)
fmt.Println("demo:", demo)
// output: demo: map[]
}
创建channel
make(chan Type, size)
使用make创建channel,第一个参数是channel类型。size表示缓冲槽大小,是一个可选的大于或等于0的整型参数,默认size = 0。当缓冲槽不为0时,表示通道是一个异步通道。
package main
import "fmt"
func main() {
demo := make(chan int, 10)
fmt.Println("demo:", demo)
}
注意事项
make和new的区别
new的作用是初始化一个指向类型的指针(*T)。使用new函数来分配空间,传递给new函数的是一个类型,不是一个值。返回的是指向这个新分配的零值的指针。
make的作用是为slice、map或chan初始化并返回引用(T)。make仅仅用于创建slice、map和channel,并返回它们的实例。
map
map是一种无序的基于key-value
的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
map定义
Go语言中 map
的定义语法如下:
map[KeyType]ValueType
其中,
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
map基本使用
map中的数据都是成对出现的,map的基本使用示例代码如下:
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
输出:
map[小明:100 张三:90]
100
type of a:map[string]int
map也支持在声明的时候填充元素,例如:
func main() {
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
fmt.Println(userInfo) //
}
判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]
举个例子:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
map的遍历
Go语言中使用for range
遍历map。
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
但我们只想遍历key的时候,可以按下面的写法:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
使用delete()函数删除键值对
使用delete()
内建函数从map中删除一组键值对,delete()
函数的格式如下:
delete(map, key)
其中,
- map:表示要删除键值对的map
- key:表示要删除的键值对的键
示例代码如下:
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
delete(scoreMap, "小明")//将小明:100从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}
按照指定顺序遍历map
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
元素为map类型的切片
下面的代码演示了切片中的元素为map类型时的操作:
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
值为切片类型的map
下面的代码演示了map中值为切片类型的操作:
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
练习题
- 写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中how=1 do=2 you=1。
- 观察下面代码,写出最终的打印结果。
func main() {
type Map map[string][]int
m := make(Map)
s := []int{1, 2}
s = append(s, 3)
fmt.Printf("%+v\n", s)
m["q1mi"] = s
s = append(s[:1], s[2:]...)
fmt.Printf("%+v\n", s)
fmt.Printf("%+v\n", m["q1mi"])
}
go rand seed
rand.Seed(time.Now().UnixNano())
randInsert := rand.Intn(4) // [0, 4)
1.time.Now().Hour() :返回当前时间的小时
2.time.Now().Unix():返回unix时间戳
3.time.Now().UnixNano():64位时间戳
如果每次调rand.Intn()前都调了rand.Seed(x),每次的x相同的话,每次的rand.Intn()也是一样的。
两种解决方案:
-
只调一次rand.Seed():在全局初始化调用一次seed,每次调rand.Intn()前都不再调rand.Seed()。
-
调多次rand.Seed(x),但每次x保证不一样:每次使用纳秒级别的种子。强烈不推荐这种,因为高并发的情况下纳秒也可能重复。
Printf() 是把格式化字符串输出到标准到标准输出(一般是屏幕,可以重定向)
Printf() 是和标准输出文件(stdout)关联的,Fprintf 则没有这个限制
Sprintf() 是把格式化字符串输出到指定的字符串中,可以用一个变量来接受,然后在打印
Fprintf() 是把格式字符串输出到指定的文件设备中,所以参数比Printf 多一个文件指针*File
主要用于文件操作,Fprintf() 是格式化输出到一个 Stream ,通常是一个文件
append函数的使用
作用:在原切片的末尾添加元素
Go语言中type的用法
Go语言中type的用法:
1.定义结构体类型
2.类型别名
3.定义接口类型
4.定义函数类型
1.定义结构体类型
结构体可用于用户自定义数据类型和进行面向对象编程。
type Person struct {
name string
age int
sex bool
}
2.类型别名
type str string
str类型与string类型等价
例子:
type str string
func main () {
var myname str = "Ling"
fmt.Printf("%s",myname)
}
3.定义接口
type Shaper interface {
Area() float64
}
接口定义了一个 方法的集合,但是这些方法不包含实现代码,它们是抽象的,接口里也不能包含变量。
注意实现接口可以是结构体类型,也可以是函数类型。
4.定义函数类型
type functinTyoe func(int) bool // 声明了一个函数类型
Go语言的语法糖
go 变量前三个点与后三个点
做为形参的参数前的三个点意思是可以传0到多个参数
变量后三个点意思是将一个切片或数组变成一个一个的元素,俗称将数组打散.
函数
Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
函数定义
Go语言中定义函数使用func
关键字,具体格式如下:
func 函数名(参数)(返回值){
函数体
}
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。 - 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。 - 函数体:实现指定功能的代码块。
我们先来定义一个求两个数之和的函数:
func intSum(x int, y int) int {
return x + y
}
函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:
func sayHello() {
fmt.Println("Hello 沙河")
}
函数的调用
定义了函数之后,我们可以通过函数名()
的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:
func main() {
sayHello()
ret := intSum(10, 20)
fmt.Println(ret)
}
注意,调用有返回值的函数时,可以不接收其返回值。
参数
类型简写
函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:
func intSum(x, y int) int {
return x + y
}
上面的代码中,intSum
函数有两个参数,这两个参数的类型均为int
,因此可以省略x
的类型,因为y
后面有类型说明,x
参数也是该类型。
可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。
注意:可变参数通常要作为函数的最后一个参数。
举个例子:
func intSum2(x ...int) int {
fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
调用上面的函数:
ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:
func intSum3(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}
调用上述函数:
ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
注意:本质上,函数的可变参数是通过切片来实现的。
返回值
Go语言中通过return
关键字向外输出返回值。
多返回值
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()
将所有返回值包裹起来。
举个例子:
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
返回值命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return
关键字返回。
例如:
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
返回值补充
当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}