Go 语言入门指南:基础语法和常用特性解析 (二) | 青训营

99 阅读10分钟

Go 语言入门指南:基础语法和常用特性解析 (二)

入门 Go 语言,走近 Go 语言,体会 Go 语言的独特魅力。从基础语法和常用特性解析入手...

我们继续从 字符串 数组 slice map struct 淦(肝)起,本文同样引自 CSDN 中 Go 语言入门指南:基础语法和常用特性解析_框住的博客-CSDN博客 的文章,让我们畅游在 Go 语言的海洋里吧,Go!

五、字符串

  • Go 语言字符串按字节存储,不同字符占用不同数目的字节。
  • 字符串的字符按 unicode 编码存储,不同的字符按 1~4 个字节存储。其中,中文汉字占用 3 个字节,英文占用 1 个字节。
  • 字符串索引访问是按字节访问的,而不是字符。

1692352162480.png

5.1 rune

  • unicode 通常用 4 个字节来表示,对应Go语言的字符 rune 占 4 个字节
  • rune 类型是一个衍生类型,在内存里面使用 int32 类型的 4 个字节存储
type rune int32

5.2 Unicode 和 UTF-8

  1. Unicode 支持超过一百种的语言和十万字符的码点。
  2. UTF-8 是 Unicode 标准的一种实现。
  3. UTF-8 特点:
  • 字符长度可变,长度1到4字节不等。
  • 第一个 bit 为0,那么长度为1字节,使用剩余7位存放字符,正好能覆盖 ASCII 码字符集。
  • 前两位是10,则表示长度为两个字节,第二个字节以0开头。
  • 对于三个字节的 UTF-8 码。这三个字节分别是110、10和0。

5.3 按字节访问

package main

import "fmt"

func main() {
	var s = "嘻哈china"
	for i:=0;i<len(s);i++ {
		fmt.Printf("%d %x ", i, s[i])
	}
 
}

-----------
0 e5 1 98 2 bb 3 e5 4 93 5 88 6 63 7 68 8 69 9 6e 10 61

5.4 按字符rune访问

package main

import "fmt"

func main() {
	var s = "嘻哈china"
	for codepoint, runeValue := range s {
		fmt.Printf("%d %d ", codepoint, int32(runeValue))
	}
}

-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97

对字符串进行 range 遍历,每次迭代出两个变量 codepointruneValue

  • codepoint:字符起始位置
  • runeValue:对于的 unicode 编码(类型是rune)

5.5 特点

  • 字符串是只读的,不支持按索引修改。
  • 字符串支持切片成子串。

字节数组和字符串的转换

借助 []byte()string() 即可:

var s1 = "hello world"     
var b = []byte(s1)     // 字符串转字节数组
var s2 = string(b)	   // 字节数组转字符串

/*
b = [104 101 108 108 111 32 119 111 114 108 100]
s2 = hello world
*/

六、数组

在 Go 语言中,由于数组长度不可变,数组的使用频率并不高。大部分时候使用 slice(切片),因为 slice 比数组更灵活,长度是可变的。

6.1 数组的定义

6.1.1 使用默认初始值

var a [3]int

上述代码定义了长度为 3 的整型数组,三个元素都是默认初始值 0。

6.1.2 定义并初始化

var b [3]int = [3]int{1, 2, 3}
var b = [3]int{1, 2, 3}

6.1.3 省略数组长度

c := [...]int{1, 2, 3}

上述代码的数组长度由后面元素的个数决定。这里数组的长度为 3。

6.1.4. 索引赋值初始化

d := [...]int{4, 4:1, 1:2}

// d : {4, 2, 0, 0, 1}
  • 并没有给所有的元素赋值,而且赋值使用了 index:value 的方式。4:1 的意思是给下标为 4 的元素赋值为 1。
  • 数组长度根据最大下标确定。

6.2 数组特点

  • [3]int[4]int 是两种不同的数据类型,这两种不同的数组在编译时就已经确定。把 [4]int 类型的变量赋值给 [3]int 类型的变量会报错。
  • Go 语言数组长度是数组的一部分,并非向 C 语言那样只存储数组的地址。
  • 在分配内存底层空间时,数组的元素是紧挨着分配到固定位置的,这也是数组长度不可变的原因。

6.3 数组是默认按值传递

  • Go语言数组是传递值的,不同于其他语言的数组都是传递引用的。
  • 值传递涉及数组的拷贝,会浪费性能,但形参数组的修改不会影响原数组。

给出一个实例 ——修改数组的0索引位置的元素

方式解释
直接传数组传数组地址
值传递引用传递(指针)
package main

import "fmt"

func main() {
	var a [3]int = [3]int{1, 2, 3}
    
	change_arr_by_value(a)
	fmt.Println(a[0]) // 1 按值传递,修改失败

	change_arr_by_reference(&a)
	fmt.Println(a[0]) // 999 按索引传递,修改成功
}

func change_arr_by_value(arr [3]int) {
	arr[0] = 999
}

func change_arr_by_reference(arr *[3]int) {
	arr[0] = 999
}

七、slice

slice(切片)是一个拥有相同类型元素的可变长序列,且 slice 定义和数组的定义很像。

  • slice是通过指向底层数组来存储数据,而且可能有多个slice指向同一个底层数组。如果有任一切片指向这个数组,底层数组就因处于使用状态而无法被删除。
  • 极端情况,很小的切片指向很大底层数组,会造成空间的浪费。

7.1 原型

type SliceHeader struct {
	Data uintptr	// 指针
	Len int			// 长度
	Cap int			// 容量
}

说明

  • 指针指向 slice 开始访问的第一个元素。
  • 长度是切片的长度,及元素个数。
  • 容量是切片能访问的最大元素个数。 slice 的容量 ≥ silice 的长度

切片的底层是数组,Go 语言的切片对应着底层数组。一个底层数组可以对应多个 slice。

7.2 基本操作

7.2.1 定义

s := []int{1, 2, 3, 4, 5}

如果在方括号中指定了长度或写入省略号,那就是一个数组。

ss := make([]int, 10)

make 函数定义了 []int 类型的切片,长度为 10,元素默认值为 0。

7.2.2 释放slice

ss = nil

如果将 slice 赋值为 nil,垃圾回收机制将会回收 slice 的空间。

7.2.3 获取长度和容量

len(slice)
cap(slice)

7.2.4 切片

  • 切片 s[i:j] 是左闭右开,索引区间是[i, j)
  • 左边界 i 不写,默认是 0。右边界 j 不写,默认是切片的容量 cap(slice)。
  • s[:] 是整个切片。
s := []int{0, 1, 2, 3, 4, 5}
s[0:3] // {0, 1, 2}
s[:] // {0, 1, 2, 3, 4, 5}

7.2.5 数组的切片

  • 对数组切片的修改会影响原数组。slice默认是引用传递
a := [...]int{1, 2, 3, 4, 5}
ss := a[1:3]
ss[0] = 99
ss[1] = -99

fmt.Println(ss)  // [99, -99]
fmt.Println(a) 	 // [1, 99, -99, 4, 5]

7.3 append

  • slice 的长度是动态的。
  • append 可以在原来的切片上插入元素,可以在开头、结尾、指定位置插入元素或其他 slice。
  • 数组不能直接插入,要转换成切片才能添加。
s = []int{1, 2, 3}			// 定义切片
arr = [3]int{66, 77, 88}	// 定义数组

7.3.1 头插

ss = append(a[:], ss...)
ss = append([]int{1}, ss...)

注意

  • ss... :把切片 ss,拆解成一个个元素。之后将依次插入。
  • a[:] :取数组的切片。

7.3.2 尾插

ss = append(ss, 1)
ss = append(ss, a[:]...)

7.3.3 任意位置插入

ss = append(ss[:1], append(a[:], ss[1:]...)...) // 在 ss 的索引为 1 的位置插入

7.4 slice的增长过程

  • 切片的底层是数组,如果使用append函数向切片中添加元素是,实际上是把元素放到切片的底层数组。
  • 如果底层数组满了,没有空间,这时再向切片添加元素,Go只能创建一个更大的新数组,将原来的数组复制到新数组,并把新数组作为切片的底层数组。

7.4.1 代码演示

s1 := []int{1, 2, 3}
fmt.Printf("长度:%d,容量:%d,数组地址:%p\n", len(s1), cap(s1), s1)
s1 = append(s1, 4)
fmt.Printf("长度:%d,容量:%d,数组地址:%p\n", len(s1), cap(s1), s1)
s1 = append(s1, 5)
fmt.Printf("长度:%d,容量:%d,数组地址:%p\n", len(s1), cap(s1), s1)
s1 = append(s1, 6)
fmt.Printf("长度:%d,容量:%d,数组地址:%p\n", len(s1), cap(s1), s1)
s1 = append(s1, 7)
fmt.Printf("长度:%d,容量:%d,数组地址:%p\n", len(s1), cap(s1), s1)
s1 = append(s1, 8)
fmt.Printf("长度:%d,容量:%d,数组地址:%p\n", len(s1), cap(s1), s1)


/*output
	长度:3,容量:3,数组地址:0xc00001e0c0
    长度:4,容量:6,数组地址:0xc00001a120
    长度:5,容量:6,数组地址:0xc00001a120
    长度:6,容量:6,数组地址:0xc00001a120
    长度:7,容量:12,数组地址:0xc00005c060
    长度:8,容量:12,数组地址:0xc00005c060
*/

7.4.3 切片变换过程

1692497048072.png

7.5 copy

切片元素的赋值可利用 copy 函数

a1 := []{1,1,1,1,1}
b1 := []{-1,-1,-1}
copy(a1, b1)  // 将 b1 复制到 a1
// a1 : [-1,-1,-1,1,1]

a2 := []{2,2,2,2,2}
b2 := []{-2,-2,-2}
copy(b2, a2)  // 将 b1 复制到 a1
// b2 : [2,2,2]

注意 要理解复制到的含义。

  • copy函数只涉及元素值的拷贝,不涉及新空间的创造。
  • copy 的两个参数必须是 slice,不能是数组。

八、map

  • map(映射)是 Go 语言提供的 key-value(键值对)形式的无序集合。
  • map 的底层是 Hash(哈希)表。
  • 键值对的键有唯一性的要求,通过键来获取值和更新值。

8.1 定义形式

map[k]v

  • k 是键,同一 mao 中所有的 key 是同一数据类型,而且必须能进行 == 的比较运算 。
  • boll 类型作为 k 并不灵活,而浮点型作为 k,可能会因为精度问题而导致异常。

8.2 ## 基本操作

8.2.1 创建 map

// 方式一
m1 := make(map[string]int)

// 方式二
m2 := map[string]int{
    "k1" : 11,
    "k2" : 22,
}

8.2.2 访问元素

map[k]

比如 val,ok = m2["k1"]。根据 key 找 val,如果 key 不存在,ok=false。

8.2.3 添加元素

map[key] = value

注意:key 如果存在,则用 value 更新值。如果不存在,则添加 key-value 对。

8.2.4 删除元素

delete(map, k)

比如 delete[m2, "k2"]

注意 delete 函数删除不存在的 key 时,不会出错。

8.2.5 遍历map

for k, v := range m2 {
    fmt.Println(k, v)
}

8.3 特点

  • map 在元素赋值之前必须初始化。使用上面两种方式均可。如果只声明 map,使用则会出错。
  • map 的默认初始值是 nil,未初始化的 map 是 nil。尽管如此,未初始化的 map 执行删除元素、len 操作、range 操作或查找元素时都不会报错。但未初始化之前进行元素赋值操作会出错。
var m1 map[string]int
m1["k1"] = 12  // error: assignment to nil map 

九、struct

struct是复合类型,而非引用类型。复合类型是值传递,而引用类型是引用传递。

9.1 结构体定义

type Person struct {
	name string
	gender int
	age int
}

var p1 Person

p2 := Person{"Jack", 1, 18}

p3 := Person{name:"Scott", gender:1, age:18}

注意 struct成员的可见性是通过首字母大小写控制,首字母小写仅本报可见,首字母大写则包外也可访问。

9.2 成员访问

p3.name

9.3 结构体指针

var pp *Person

注意 结构体指针必须初始化后才可以使用,因为如果仅仅声明结构体指针类型变量,其默认值初始值是 nil。

结构体指针解释
pp := new(Person)创建指向 Person 结构体的指针 pp
pp.name = "Json"结构体指针访问成员也是直接.运算符,这是 Go 的语法糖

9.4 make和new

  • make 函数用于 slice、map 和 chan 进行内存分配,它返回的不是指针,而是上面三个类型中的某一个类型本身。
  • new 函数返回初始化的类型对应的指针,new 函数主要用于 struct 初始化中,其他场景应用较少。