likes
comments
collection
share

Go语言系列:两三天也学不完Go语言的进阶语法知识

作者站长头像
站长
· 阅读数 25

前言

这次接着之前Go语言的学习,继续来学习Go语言的基础知识。上一篇Go语言系列:半天学完Go语言的最最基础的语法知识文章需要半天时间,那这次预计需要一天时间了,甚至更多,所以这次也要耐心看完哦。

Go语言的基础语法

上次聊到Go语言的常量,这次书接上回,接着往下聊,慢慢地往后深入→

一、枚举

Go语言中其实是没有枚举类型的,但是,可以使用常量iota来模拟枚举。

iota常量生成器用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota默认会被置为0,然后在每一个有常量声明的行自动+1

package main

import "fmt"

// 定义一个名为 Sex 类型, 实际类型为 int
type Sex int

// 定义性别男女
const (
	Woman Sex = iota // 将枚举值Woman定义为Sex类型,并搭配iota开始生成枚举值,默认从0开始
	Man
)

func main() {
        // 输出枚举值
	fmt.Println(Woman, Man)
        
        // 使用枚举类型,并赋值
	var sex Sex = Man
	fmt.Println(sex)
}

输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

从打印的结果可以看到,Woman枚举值定义了0, 那么Man的值会自动+1,变成了1。看来是没有问题的。

当然,iota除了每次自增1以外,还可以利用iota来完成一些更复杂的操作。如下:

package main 

import "fmt" 

const ( 
    val1 = 1 << iota // 移位操作,左移一位 
    val2 
    val3 
    val4 
) 

func main() { 
    // 输出枚举整型值 
    fmt.Printf("%d %d %d %d\n", val1, val2, val3, val4) 

    // 输出枚举二进制格式的值 
    fmt.Printf("%b %b %b %b", val1, val2, val3, val4) 
}

输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

在实际应用场景中,通常需要获取枚举值对应的字符串描述。可以如下操作:

package main 

import "fmt" 

// 定义一个名为 Sex 类型 
type Sex int 

const ( 
    Woman Sex = iota // 开始生成枚举值,默认从 0 开始 
    Man 
) 

// 定义一个 Sex 类型的方法 String(), 返回字符串 
func (s Sex) String() string { 
    switch s { 
        case Woman: 
            return "女" 
        case Man: 
            return "男" 
    } 
    return "N/A" 
} 

func main() { 
    // 输出枚举 Woman 的字符串描述,以及整型值 
    fmt.Printf("%s %d", Woman, Woman) 
}

输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

二、指针

Go语言中,指针有两个核心概念:

  • 类型指针:允许对这个指针类型的数据进行修改。传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算。
  • 切片:由指向其实元素的原始指针、元素数量和容量组成。

Go语言中,通过&操作符对变量进行“取地址”操作,如下:

p := &v // v 的类型为 T

上述代码中,v表示被取地址的变量,取到的地址变量p进行接收,p的类型为*T,成为T的指针类型。*代表指针。

来看下面这个指针的取地址示例:

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

注意:代码每次运行的结果是不同的,表示 num 和 website 两个变量在运行时的地址。

总结: 变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值表示这个地址。

现在能通过 & 获取变量的指针,那么要如何通过指针取值呢?代码如下:

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

总结:变量、指针地址、指针变量、取地址、取值的相互关系和特性见下

  • 对变量进行取地址 (&) 操作,可以获取这个变量的指针变量;
  • 指针变量的值是指针地址;
  • 对指针变量进行取值(*)操作,可以获取指针变量指向的实际值。

下面就继续来看使用指针修改值,如下:

package main

import "fmt"

// 数值交换函数
func swap(a, b *int)  {
	// 取 a 指针的值,赋给临时变量 t
	t := *a

	// 取 b 指针的值,赋给 a 指针指向的变量
	*a = *b

	// 将 a 指针的值赋给 b 指针指向的变量
	*b = t
}

func main()  {
	// 声明两个变量 x, y, 值分别为 1,2
	x, y := 1, 2

	// 交换变量值
	swap(&x, &y)

	// 输出交换后的 x, y 值
	fmt.Println(x, y)
}

上述代码中输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

那如果把上面代码中的swap()函数改为交换的是指针值呢?

func swap(a, b *int)  {
    b, a = a, b
}

func main()  {
    // 声明两个变量 x, y, 值分别为 12
    x, y := 1, 2

    // 交换变量值
    swap(&x, &y)

    // 输出交换后的 x, y 值
    fmt.Println(x, y)
}

代码输出如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

可以看到,值交换失败了。上面swap()函数交换的是ab的地址,交换完毕后,它们实际指向的值并没有发生改变。这就好比放在桌子上的两个钱包,将位置交换后,里面存放的钱并没有发现改变一样。

Go语言中还提供了new()函数来创建指针,代码如下:

str := new(string) 
*str = "黑土豆" 

fmt.Println(*str)

注意:new()函数可以创建一个对应类型的指针,同时会分配内存,被创建的指针指向的值为默认值。

三、数组

Go语言中,数组一旦声明,那么大小就确定了,仅可以修改数组成员,但是不能改变大小。

Go语言中定义数组的格式如下:

var 数组变量名 [元素数量]T

说明:

  • 数组变量名: 定义一个数组的变量名;
  • 元素数量: 定义数组的大小;
  • T可以是任意基本类型,甚至可以是数组本身,若为数组,则可以实现多维数组。

直接来个示例就明了了,如下:

package main

import "fmt"

func main()  {
    // 定义一个变量为 arr, 成员类型为 string, 大小为 3 的数组
    var arr [3]string

    // 赋值操作
    arr[0] = "a"
    arr[1] = "b"
    arr[2] = "c"

    fmt.Println(arr)
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

那下面来看Go语言中怎么初始化数组,分别有两种方式,如下:

  1. 定义数组的时候,将数组提前初始化好
var arr = [3]string{"a", "b", "c"}
  1. 将定义数组大小的操作交给编译器,让编译器在编译时,根据元素的个数来确定大小
var arr = [...]string{"a", "b", "c"}

...表示让编译器来确定数组大小。如上的代码编译器会自动将这个数组的大小设置为3

Go语言中又是怎么遍历数组的呢?通过for range来遍历数组,如下:

package main

import "fmt"

func main()  {
    // 定义一个变量为 arr, 成员类型为 string, 大小为 3 的数组
    var arr = [...]string{"a", "b", "c"}

    for index, v := range arr {
            fmt.Printf("index: %d, value: %s\n", index, v)
    }
}

上面代码中,index表示数组当前的下标, v表示当前元素的值。

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

四、切片

Go语言中切片和数组类似,都是数据集合。和数组不同的是,切片是一块动态分配大小的连续空间

如何声明切片?切片的声明格式如下:

var name []T

说明:

  • name表示切片变量名;
  • T表示切片类型。

来看一个具体的示例代码, 如下:

package main

import "fmt"

func main()  {
    // 声明整型切片
    var numSlice []int

    // 声明字符串切片
    var strSlice []string

    // 声明一个空切片, {} 表示已经分配内存,但是切片里面的元素是空的
    var numSliceEmpty = []int{}

    // 输出3个切片
    fmt.Println(numSlice, strSlice, numSliceEmpty)

    // 输出3个切片大小
    fmt.Println(len(numSlice), len(strSlice), len(numSliceEmpty))

    // 切片判定是否为空结果
    fmt.Println(numSlice == nil)
    fmt.Println(strSlice == nil)
    fmt.Println(numSliceEmpty == nil)
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

Go语言中可以通过make()函数动态的创建一个切片。格式如下:

make( []T, size, cap )

说明:

  • T:切片中元素的类型;
  • size:表示为这个类型分配多少个元素;
  • cap:预分配的元素数量,该值设定后不影响size, 表示提前分配的空间,设置它主要用于降低动态扩容时,造成的性能问题。

示例代码如下:

package main

import "fmt"

// 使用 make() 函数构造切片
func main()  {
    a := make([]int, 3)
    b := make([]int, 3, 10)

    fmt.Println(a, b)
    fmt.Println(len(a), len(b))
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

注意:len()函数计算的是元素的个数,与切片容量无关。

Go语言中可以通过append()可以为切片动态添加元素。代码如下:

package main

import "fmt"

// 使用 append() 函数为切片添加元素
func main()  {
    // 声明一个字符串类型的切片
    var strSlice []string

    // 循环动态向 strSlice 切片中添加 6 个元素,并打印相关参数
    for i := 0; i < 6; i++ {
        line := fmt.Sprintf("abc %d", i)
        strSlice = append(strSlice, line)

        fmt.Printf("len: %d, cap: %d, pointer: %p, content: %s\n", len(strSlice), cap(strSlice), strSlice, strSlice[i])
    }
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

注意:通过上面的代码输出,会发现len()并不等于cap。这是因为当切片空间不足以容纳足够多的元素时,切片会自动进行扩容操作, 扩容规律按切片容量的2倍进行扩容,如 1、2、4、8、16 ....

另外,append()函数除了添加一个元素外,还能一次性添加多个元素。如下:

package main

import "fmt"

func main()  {
    var strSlice []string

    // 添加一个元素
    strSlice = append(strSlice, "abc")

    // 添加多个元素
    strSlice = append(strSlice, "def", "ghi", "jkl")

    // 添加切片
    list := []string{"mno", "pqr"}
    // list 后面的 ... 表示将 list 整个添加到 strSlice 切片中
    strSlice = append(strSlice, list...)

    fmt.Println(strSlice)
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

Go语言中可以通过从数组或切片生成新的切片。格式如下:

slice [开始位置:结束位置]

说明:

  • slice表示切片目标;
  • 开始位置和结束位置对应目标切片的下标。

从数组中生成切片,代码如下:

package main

import "fmt"

// 从数组中生成切片
func main()  {
    var arr = [5]int{1, 2, 3, 4, 5}

    fmt.Println(arr, arr[1:2])
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

注意:[2]arr[1:2]切片操作的结果(取出的元素不包括结束位置的元素)。

Go语言中可以从指定范围中生成切片,示例代码如下:

package main

import "fmt"

// 从指定范围中生成切片
func main()  {
    var arr = [10]int{}

    // 向数组中添加元素
    for i := 0; i < 10; i++ {
            arr[i] = i + 1
    }

    // 指定区间
    fmt.Println(arr[5:9])

    // 中间到尾部所有元素
    fmt.Println(arr[6:])

    // 开头到中间所有元素
    fmt.Println(arr[:8])

    // 切片本身
    fmt.Println(arr[:])
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

总结:

  • 若不填写结束位置,如arr[5:], 则表示从下标5置到数组的结束位置。
  • 若不填写开始位置,如arr[:5],则表示从0到下标5的位置。
  • 若开始位置和结束位置都不填写,如arr[:], 则会生成一个和原有切片一样的切片。

Go语言中若把切片的开始位置和结束位置都设置为0,则会生成一个空的切片,即重置切片。示例代码如下:

package main

import "fmt"

// 重置切片
func main()  {
    var arr = [10]int{}

    // 向数组中添加元素
    for i := 0; i < 10; i++ {
            arr[i] = i + 1
    }

    fmt.Println(arr[0:0])
}

Go语言中可以通过copy()函数将一个切片中的数据复制到另一个切片中,使用格式如下:

copy( destSlice, srcSlice []T) int

说明:

  • srcSlice代表源切片;
  • destSlice代表目标切片。

注意:目标切片必须有足够的空间来装载源切片的元素个数。返回值为整型,表示实际发生复制的元素个数。

package main

import "fmt"

// 复制切片元素到另一个切片
func main()  {
    // 源分片
    srcSlice := make([]int, 10)

    // 给源分片赋值
    for i := 0; i < 10; i++ {
            srcSlice[i] = i
    }

    // 目标分片
    destSlice := make([]int, 10)

    // 将 srcSlice 分片的数据复制到 destSlice 中
    copy(destSlice, srcSlice)

    fmt.Println(srcSlice)
    fmt.Println(destSlice)
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

Go语言中并没有提供特定的函数来删除切片中元素,但是可以利用切片的特性来达到目的。如下:

package main

import "fmt"

// 从切片中删除元素
func main()  {
    // 声明一个字符串类型的切片
    arr := []string{"a", "b", "c", "d", "e", "f", "g"}

    // 指定删除位置,也就是 u 元素
    index := 1

    // 打印删除位置之前和之后的元素, 
    // arr[:index] 表示的是被删除元素的前面部分数据,
    // arr[index+1:] 表示的是被删除元素后面的数据
    fmt.Println(arr[:index], arr[index+1:])

    // 将删除点前后的元素拼接起来
    arr = append(arr[:index], arr[index+1:]...)

    fmt.Println(arr)
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

注意:Go语言中切片删除元素的本质即:以被删除元素为分界点, 将前后两个部分的内存重新连接起来。

五、字典

Go语言中提供的字典容器为 map, map使用散列表(hash)实现。格式代码如下:

map [keyType]valueType

说明:

  • keyType表示键类型;
  • valueType表示键对应的值类型。

注意:键和键对应的值总是以一对一的形式存在。

来看下面这段map的示例代码,如下:

package main

import "fmt"

func main()  {
    // 定义一个键类型为字符串,值类型为整型的 map
    m := make(map[string]int)

    // 向 map 中添加一个键为 “abc”,值为 1 的映射关系
    key := "abc"
    m[key] = 1

    // 输出 map 中键为 “abc” 对应的值
    fmt.Println(m[key])

    // 尝试输出一个不能存在的键,会输出该值类型的默认值 
    n := m["def"]

    fmt.Println(n)
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

注意:如果尝试从map中获取一个并不存在的键(key), 此时会输出值类型的默认值,整型的默认值为0

map还存在另外一种初始化方式, 通过大括号的方式来初始化字典map, 冒号左边的是键(key) , 右边的是值(value),键值对之间使用逗号分隔。代码如下:

m := map[int](string){
    1: "a",
    2: "b",
    3: "c",
}

Go语言中字典map的遍历需要使用for range循环,代码如下:

package main

import "fmt"

// 遍历map
func main()  {
    m := map[int](string){
        1: "a",
        2: "b",
        3: "c",
    }

    // 通过 for range 遍历, 获取 key, value 值并打印
    for key, value := range m {
        fmt.Printf("key: %d, value: %s\n", key, value)
    }
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

如果只需要遍历值,也可以通过匿名变量来实现:

for _, value := range m { 
    fmt.Printf("value: %s\n", value) 
}

只遍历键时,通过下面这种方式:

for key := range m { 
    fmt.Printf("key: %d\n", key) 
}

注意: 字典map是一种无序的数据结构,不要期望输出时按照一定顺序输出。如果需要按顺序输出,请使用切片来完成。

Go语言中是通过内置函数delete()来删除键值对,格式如下:

delete(map, 键)

说明:

  • map表示要删除的目标map对象;
  • 键表示要删除的mapkey键。

示例代码如下:

package main

import "fmt"

// 删除字典 map 中键值对
func main()  {
    m := map[int](string){
        1: "a",
        2: "b",
        3: "c",
    }

    // 删除 map 中键为 1 的键值对
    delete(m, 1)

    // 通过 for range 遍历, 获取 key, value 值并打印
    for key, value := range m {
        fmt.Println(key, value)
    }
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

六、list(列表)

列表是一种非连续存储的容器,由多个节点组成,节点通过一些变量将彼此串联起来。列表常见的数据结构有: 单链表、双链表等

Go语言中,列表的实现都在container/list包中,内部实现原理是双链表。列表能够方便高效地进行元素的删除、插入操作。

Go语言中list的初始化方法有两种:New和声明。如下:

  1. 通过container/list包中的New方法来初始化list, 如下:
变量名 := list.New()
  1. 通过声明初始化list,如下:
var 变量名 = list.List

那在Go语言中是如何向list(列表)中添加元素的呢?通过双链表支持往队列前面或后面添加元素,对应的方法分别为:

  • PushFront
  • PushBack

示例代码如下:

l := list.New()

l.PushFront("abc")
l.PushBack("xyz")

关于list(列表)插入元素的方法,如下表所示:

方法功能
InsertAfter(v interface{}, mark *Element) *Elementmark点后面插入元素
InsertBefore(v interface{}, mark *Element) *Elementmark点前面插入元素
PushFrontList(other *List)添加other列表中的元素到头部
PushBackList(other *List)添加other列表中的元素到尾部

Go语言中list(列表)的插入函数的返回值是一个*list.Element结构,可以通过它来完成对列表元素的删除,代码如下:

package main

import (
    "container/list"
    "fmt"
)

func main()  {
    l := list.New()

    // 头部添加字符串
    l.PushFront("abc")

    // 尾部添加字符串
    l.PushBack("xyz")

    // 尾部添加一个整型,并保持元素句柄
    element := l.PushBack(1)

    // 在 1 之后添加字符串 2
    l.InsertAfter("2", element)

    // 在 1 之前添加字符串 0
    l.InsertBefore("0", element)

    // 删除 element 对应的元素
    l.Remove(element)
    for i := l.Front(); i != nil; i = i.Next() {
        fmt.Println(i.Value)
    }
}

代码输出结果如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

如果不执行l.Remove(element),则list保存的元素就是如下所示:

Go语言系列:两三天也学不完Go语言的进阶语法知识

Go语言中遍历list(列表) 需要搭配Front()函数获取头元素,遍历过程中,只要元素不为空则可继续调用Next函数往下遍历,从上面的示例就可以看到,代码如下:

package main

import (
    "container/list"
    "fmt"
)

// 遍历 list (列表)
func main()  {
    l := list.New()

    // 头部添加字符串
    l.PushFront("abc")

    // 尾部添加字符串
    l.PushBack("xyz")

    // 遍历
    for i := l.Front(); i != nil; i = i.Next() {
            fmt.Println(i.Value)
    }
}

注意,在for语句遍历中:

  • 其中i := l.Front()表示初始赋值,用来获取列表的头部下标;
  • 然后每次会循环会判断i != nil,若等于空,则会退出循环,否则执行i.Next()继续循环下一个元素;

代码输出如下:

Go语言系列:两三天也学不完Go语言的进阶语法知识

至此,Go语言的进阶语法知识就介绍完了,对于很久没有看Go语言的我来说,一时半会儿还真没有吃透,只有后续通过项目来实践掌握。希望大家看完能有所收获!

往期精彩文章

后语

小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

Go语言系列:两三天也学不完Go语言的进阶语法知识