Go语言进阶之路(一):变量、类型、数组、切片、字典和结构体

一 类型

Go语言中内置的基础类型和各类型的初始值为:

啥叫初始值?也就是没有初始化的值,比如:

<code>var a int
fmt.Println(a) // 变量a没有被代码初始化,a现在的值就是初始值,此处打印出0。/<code>

等一下,这里面混进了两个比较奇怪的东西,rune和uintptr。rune是Unicode类型,和int32等价,在后续的文章中讲string的时候会重点介绍,uintptr是无符号整数,存放的是指针的值,可以理解为用来保存指针。


Go语言中除了这些基础类型,还有数组、切片、字典、指针、结构体、通道、函数、接口这些类型,后续的文章会详细讲这些。

二 变量

Go语言定义变量使用var关键字。定义变量时可以选择指定类型,或者让编译器自动推导出类型,可以指定初始化值,也可以使用编辑器的初始值。如下:

<code>var a int // a没有初始化值,会使用编译器中int的默认值0。
var b int = 2 // 声明变量时指定类型,同时指定初始化值。
var c = 2 // 声明变量时指定初始化值,让编译器推导出类型。


var d, e int = 3, 4 // 同时声明多个变量/<code>

Go语言中还有一种简短的声明变量方式,即使用“:=”。

<code>b := 2 // 简短的声明变量b。/<code>

注意,“:=”是用于声明变量的符号,使用“:=”,符号左边一定要至少有一个新生命的变量。如:

<code>b := 3 // 此行报错,b在上面已经被声明过了。

b, c := 4, 5 // 此行正确,因为符号左边至少有一个新声明的变量c。这一行执行完后,b的值就变成4了。/<code>

2.1 常量

Go语言声明常量很简单,使用const关键字就行。

<code>const a = "2"
a = "3" // 编译错误,a是常量,不能再修改它的值。/<code>

2.2 iota计数器

当我们要多个常量来表示计数器的时候,可以使用Go语言内置的常量计数器。当iota出现时,对应的变量值为0,每往下一行,变量值就加1。

<code>const (
\ta = iota // iota出现的地方,该变量值为0,即a的值为0。每往下一行,变量值就加1。
\tb // b的值为a的值加1,即1。
\tc = "str" // c设定了初始化值,破坏了iota往下的赋值规则,因此iota失效。c的值为str。


\td // 常量d未指定初始化值,自动使用上一行的初始化值,dd值为str。
)
fmt.Println(a) // 输出0
fmt.Println(b) // 输出1
fmt.Println(c) // 输出str
fmt.Println(d) // 输出e
/<code>

iota再次出现时,变量值重新为0。每往下一行,变量值就加1。

<code>const (
\ti = iota // i的值为0
\tj = iota // iota再次出现,变量值也为0,即j的值为0,每往下一行,变量值就加1。
\t_ // 跳过一个iota值
\tk
)
fmt.Println(i) // 输出0
fmt.Println(j) // 输出0
fmt.Println(k) // 输出2/<code>

iota值可以跳过,需要使用Go语言中的跳过操作符(也称为垃圾操作符),即“_”。

<code>const (
\ti = iota // iota再次出现,变量值也为0,每往下一行,变量值就加1。
\tj
\t_ // 跳过一个iota值
\tk
)
fmt.Println(i) // 输出0
fmt.Println(j) // 输出1
fmt.Println(k) // 输出3/<code>

三 逻辑判断、循环

3.1 逻辑判断

Go语言中使用if进行逻辑判断,判断条件不需要用小括号括起来,判断后执行的内容需要用大括号括起来。

<code>if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}/<code>

if语句里面也可以声明变量!声明变量的生命周期随着if结束就结束了。

<code>if num := 9; num < 0 { // num的生命周期只在此if...else中
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}/<code>

3.2 循环

Go语言中有for关键字,没有while关键字。但可以使用for来实现while。for循环使用有四种方式:

无限循环:

<code>var i = 0
for {
fmt.Println(i)
i++
}/<code>

只带条件的循环:

<code>i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1


}/<code>

<code>for n := 0; n <= 5; n++ {
if n % 2 == 0 {
continue
}
fmt.Println(n)
}/<code>

range循环:

<code>sp := []int{1, 2, 3, 4, 5}
for index, item := range sp {
fmt.Println("元素位置=", index, "元素值=", item)
}


kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\\n", k, v)
}

for k := range kvs {
fmt.Println("key:", k)
}



for i, c := range "go" {
fmt.Println(i, c)
//0 103
//1 111
}/<code>

四 数组和切片

<code>var arr = [3]int{1, 2, 3}
fmt.Println(arr) // 打印出[1, 2, 3]
fmt.Println(len(arr)) // 打印出3/<code>

声明切片可以使用make,也可以直接声明。

<code>s1 := make([]string, 3, 5) // 创建了一个长度为3,容量为5的字符串切片
s2 := make([]string, 3) // 创建了一个长度为3,容量也为3的字符串切片
s3 := []string{} // 创建了一个长度为0,容量为0的字符串切片


var s4 []string // 创建了一个长度为0,容量为0的字符串nil切片/<code>

切片和数组比较像,可以把切片理解为会自动增长的数组。切片内部有指向底层数组的指针,同时,切片还具有长度和容量。如下:

<code>var s = make([]int, 3, 5) // make第一个参数为切片的类型,第二个参数为切片的长度,第三个参数为切片的容量
s[0] = 10
s[1] = 20
s[2] = 30/<code>

则切片s的底层实现为:

当长度大于容量时,往切片中添加元素,会导致切片扩容。

<code>var s = make([]int, 3, 5)
fmt.Println(s) // 输出[0, 0, 0],s的长度为3,s的容量为5。

s = append(s, 1) // 往切片末尾添加1个元素,s的元素值现在为[0, 0, 0, 1],s的容量为5。

s = append(s, 2, 3) // 再往s末尾添加2个元素,s底层数组已经无法再容下2个元素,s自动扩容,容量翻倍,然后再往s中添加元素。s的元素值现在为[0, 0, 0, 1, 2, 3],s的容量为10。/<code>

注意:append函数会返回一个操作后的切片,一般来说,使用append操作切片后,要赋值给原来的那个切片变量,要不然可能会有预期不到的效果。如下:

<code>var s = make([]int, 3, 5) //
s1 := append(s, 1) // 往s长度的末尾添加1个元素,添加后长度仍小于容量,因此,直接操作s底层数组添加元素。s长度为3,因此在底层数组第4个位置添加元素1。

fmt.Println(s1) // 输出 [0, 0, 0, 1]
fmt.Println(len(s1)) // 输出4
fmt.Println(cap(s1)) // 输出5

// 这个时候,s变成了多少呢?
fmt.Println(s) // 输出 [0, 0, 0]
fmt.Println(len(s)) // 输出3
fmt.Println(cap(s)) // 输出5/<code>

上面这个例子可以看到,往切片中append添加元素后,如果不赋值给原来那个切片变量,那么在我们看来,就感觉切片没有任何变化一样。


在Go语言中,append导致切片扩容时,如果切片容量小于1000,总是会成倍的扩容底层数组。大于1000时,则会以1.25倍的数量扩容底层数组。

截取切片的内容

切片截取可以使用s[index:length:capability],其中第一个参数index表示截取时的起始位置,第二个参数表示长度所到的位置,第三个参数表示容量所到的位置,第三个参数可以不传。

<code>s := []int{1,2,3,4,5}  // s切片长度为5,容量为5。


s1 := s[2:4]  
fmt.Println(s1) // 输出[3, 4]/<code>

上面这个例子中,取s中坐标2(包含)到4(不包含)的元素赋值给s1,赋值后s和s1的底层数组共享了一部分内存。截取后,s1的长度为2到5,即长度为3,容量为从2开始,到s的容量末尾为止,即容量为2到5,即s1的容量为3。


带容量的切片截取:

<code>s := []int{1,2,3,4,5}  // s切片长度为5,容量为5。
s2 := s[2:3:3]
fmt.Println(s2) // 输出[3]/<code>

截取s中坐标2(包含)到坐标3(不包含)的内容给s2,同时容量为坐标2(包含)到坐标3(不包含)的长度。截取后,s2的长度为1,容量为1。

五 字典/映射(map)

Go语言中的字典类似于哈希表、Java中的Map,Python中的dict。

创建map可以用make创建,也可以直接创建。

<code>m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13

m1 := map[string]int{
"k1": 7,
"k2": 13,
}/<code>

添加元素

<code>m := make(map[string]int)
m["k1"] = 7/<code>

获取元素

<code>m := make(map[string]int)
value := m["k1"]/<code>

如果元素不存在,m["k1"]会返回类型的默认值。看下面这个例子:

<code>m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m["k4"] = 0

value := m["k3"] // m中没有k3,因此,返回m的value对应的int类型的默认值,即0。

value2 := m["k4"] // m中有k4,对应的值为0。/<code>

value和value2的值都是0,怎么判断m中是否存在某个key呢?其实,m["k3"]不仅会返回元素对应的值,还会返回一个bool值,表明这个元素是否存在于map中。看下面的例子:

<code>m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m["k4"] = 0

value, exist := m["k3"]
fmt.Println(exist) // 输出false
value2, exist := m["k4"]
fmt.Println(exist) // 输出true/<code>

删除元素

<code>delete(m1, "k1")/<code>

用for...range遍历map:

<code>m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m["k4"] = 0



for key, value := range m {
fmt.Println("key=", key, "value", value)
}

for key := range m {
fmt.Println("key=", key, "value", m[key])
}/<code>

六 结构体

Go语言中的结构体类似于C语言中的struct。

<code>type person struct {
name string
age int
}

fmt.Println(person{"Bob", 20}) // {Bob 20}
fmt.Println(person{name: "Alice", age: 30}) // {Alice 30}
fmt.Println(person{name: "Fred"}) // {Fred 0}
fmt.Println(&person{name: "Ann", age: 40}) // &{Ann 40}


s := person{name: "Sean", age: 50}
fmt.Println(s.name) // Sean
sp := &s
fmt.Println(sp.age) // 50
sp.age = 51
fmt.Println(sp.age) // 51
fmt.Println(s.age) // 51/<code>

嵌入类型/嵌入字段

Go 语言规范规定,结构体中如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。我们可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。也就是说,嵌入字段的类型既是类型也是名称。匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称。

看下面这个例子,直接把Animal这个结构体的字段嵌入到Cat中:

内嵌的结构体不仅会塞入所有字段,所有该结构体实现的方法也会一起塞入,见下面这个例子:

这里的c.show() 被转换成二进制代码后和 c.Point.show() 是等价的,c.x 和 c.Point.x 也是等价的。


我们下期一起聊聊Go语言中的函数和接口。