浅析Go语言容器之数组和切片的使用

2022-11-04,

在 Java 的核心库中,集合框架可谓鼎鼎大名:Array 、List、Set等等,随便拎一个出来都值得开发者好好学习如何使用甚至是背后的设计源码。虽然Go语言没有如此丰富的容器类型,但也有一些基本的容器供开发者使用,接下来让我们认识一下这些容器类型吧

目录
  • 序列容器
    • 数组
    • Vector
    • Deque
    • List
    • 单链表
  • 总结

    在 Java 的核心库中,集合框架可谓鼎鼎大名:ArrayListSetQueueHashMap 等等,随便拎一个出来都值得开发者好好学习如何使用甚至是背后的设计源码(这类文章也挺多,大家上网随便一搜)。

    虽然 Go 语言没有如此丰富的容器类型,但也有一些基本的容器供开发者使用,接下来让我们一一认识这些容器类型吧。

    序列容器

    序列容器存储特定类型的数据元素。目前有 5 种序列容器的实现:

    • array
    • vector
    • deque
    • list
    • forward_list

    这些序列容易可以用顺序的方式保存数据,利用这些序列容易能够编写有效的代码,重复使用标准库的模块化。

    数组

    Go 语言中的数组类型有点类似 C++ 中的数据,Go 的数组初始化定义后,在编译时就不会再变更。

    定义数组的方式如下:

    var a [10]int
    b := [5]string {"H", "e", "l", "l", "o"}
    

    [n]T 类型就表示含有 n 个类型为 T 的数组,本例中就是 a 变量表示含有 10 个 int 类型的整型数组;b 变量表示含有 5 个 string 类型的字符串数组。 数组的长度作为其类型的一部分,因此数组的长度是无法调整的。

    package main
    
    import "fmt"
    
    func main() {
    	var a [10]int
    	a[0] = 2022
    	a[1] = 2023
    	
    	fmt.Println(a[0], a[1])
    	fmt.Println(a)
    
    	b := [5]string {"H", "e", "l", "l", "o"}
    	
    	fmt.Println(b)
    }

    运行结果如下:

    Vector

    你可能会好奇,Go 语言又没有 C++ 中的 Vector 类型,为什么会举出这个例子。

    其实 Go 最初有一个 Vector 类型的实现,但在 2011 年 10 月 11 日,在 Go 语言的开发阶段被删除了。保留了现在的切片,而切片就变成了实际上更好的 Vector 实现。

    一个数组有固定的大小,但切片是一个动态、灵活的数组元素的视图,在实际中,切片比数组更为常见。

    []T 表示是一个具有类型 T 的元素切片,[]byte 是 byte slice,指元素为 byte 的 slice;[]string 是 string slice,指元素为 string 的 slice。

    切片通过指定两个切点 a[low : high],可以定义如下的 sliceExample 切片:

    sliceExample := []string{"Say", "Hello", "to", "you"}
    

    切片对比数组的最大优点就是:可以随着增加和删除来增加或减少容器的大小。我们来看一个例子:

    package main
    
    import "fmt"
    
    // remove i indexed item in a slice
    func remove(s []string, i int) []string {
    	copy(s[i:], s[i+1:])
    	return s[:len(s)-1]
    }
    
    func main() {
    	primes := [6]int{2, 3, 5, 7, 11, 13}
    
    	var s []int = primes[1:4]
    	fmt.Println(s)
    
    	sliceExample := []string{"Say", "Hello", "to", "you"}
    	sliceExample = append(sliceExample, ",My Gopher Friends~")
    
    	fmt.Println("Append Slice: ", sliceExample)
    
    	sliceExample = remove(sliceExample, 0)
    
    	fmt.Println("After Removed Item: ", sliceExample)
    }
    

    运行结果如下图:

    我们分享了 Go 语言提供的容器中的数组和切片,不管是数据还是切片,它们内部的数据类型必须是一致的(要么都是整型、要么都是字符串类型)。但数据的大小是固定,而切片可以根据元素的添加和减少动态调整容器大小。

    Deque

    Deque,即双端队列,是一个可以扩展的容器。扩展可以发生在容器的前面或后面。当队列的顶部或尾部需要经常被引用时,经常使用双端队列。

    Go 官网有一个双端队列的实现,官方地址点此处。

    下面的代码块显示了 Go 双端队列 deque 的使用:

    package main
    
    import (
    	"fmt"
    
    	"github.com/gammazero/deque"
    )
    
    func main() {
    	var q deque.Deque[string]
    	q.PushBack("I")
    	q.PushBack("love")
    	q.PushBack("learning")
    	q.PushBack("Go")
    
    	fmt.Println("队列长度为: ", q.Len())  // Prints: 4
    	fmt.Println("队首为元素:", q.Front()) // Prints: I
    	fmt.Println("队尾为元素: ", q.Back()) // Prints: Go
    
    	q.PopFront() // remove "I"
    	q.PopBack()  // remove "Go"
    
    	q.PushFront("Hello")
    	q.PushBack("World")
    
    	// Consume deque and print elements.
    	for q.Len() != 0 {
    		fmt.Println(q.PopFront())
    	}
    }

    运行结果如图:

    List

    List 在 Go 语言中有一个双链表的实现,它位于内置标准库 container/list 包中,官网地址为:https://pkg.go.dev/container/list

    我们可以直接使用这个链表的实现:

    package main
    
    import (
    	"container/list"
    	"fmt"
    )
    
    func main() {
    	// Create a new list and put some numbers in it.
    	l1 := list.New()
    
    	e4 := l1.PushBack(4)
    	e1 := l1.PushFront(1)
    	l1.InsertBefore(3, e4)
    	l1.InsertAfter(2, e1) // now l1 is [1 2 3 4]
    
    	// Iterate through list and print its contents.
    	for e := l1.Front(); e != nil; e = e.Next() {
    		fmt.Println(e.Value)
    	}
    
    	l1.MoveToBack(e1) // now l1 is [4 2 3 1]
    	listLength := l1.Len() // length is 4
    
    	fmt.Printf("l1 type: %T\n", l1)
    	fmt.Println("l1 length : :", listLength)
    	for e := l1.Front(); e != nil; e = e.Next() {
    		fmt.Println(e.Value)
    	}
    
    }

    运行结果为:

    1
    2
    3
    4
    l1 type: *list.List
    l1 length : : 4
    2
    3
    4
    1

    单链表

    最后介绍一下单链表,如果我们想实现的数据结构并没有标准的容器集成,此时我们就可以通过自己根据要求来写一个自己想要的容器类型,这里以头插法的单链表举例:

    package main
    
    import "fmt"
    
    type SinglyLinkedList struct {
    	head *LinkedListNode
    }
    
    type LinkedListNode struct {
    	data string
    	next *LinkedListNode
    }
    
    func (ll *SinglyLinkedList) Append(node *LinkedListNode) {
    
    	if ll.head == nil {
    		ll.head = node
    		return
    	}
    
    	currentNode := ll.head
    	for currentNode.next != nil {
    		currentNode = currentNode.next
    	}
    	currentNode.next = node
    }
    
    func main() {
    	ll := &SinglyLinkedList{}
    
    	ll.Append(&LinkedListNode{data: "Hello"})
    	ll.Append(&LinkedListNode{data: "Gopher"})
    
    	for e := ll.head; e != nil; e = e.next {
    		fmt.Println(e.data)
    	}
    }

    运行结果如图:

    当然,还有更多的容器方法可以等着自己去扩充,这一部分读者感兴趣可以在算法和数据结构的知识点中进行学习。

    总结

    本文介绍了 Go 语言的数组和切片类型,接着介绍了 Go 标准包 container 中的 list,最后实现了一个头插法的单链表。如果在日常开发过程中,有什么容器需要使用,可以从 pkg.go.dev/ 进行搜索,会有很多开源的 Go 优秀开源包,无论是学习还是使用,都能收获满满。

    到此这篇关于浅析Go语言容器之数组和切片的使用的文章就介绍到这了,更多相关Go语言数组 切片内容请搜索北冥有鱼以前的文章或继续浏览下面的相关文章希望大家以后多多支持北冥有鱼!

    《浅析Go语言容器之数组和切片的使用.doc》

    下载本文的Word格式文档,以方便收藏与打印。