ホームページ >バックエンド開発 >Golang >Go言語のスライスと配列とは何ですか

Go言語のスライスと配列とは何ですか

青灯夜游
青灯夜游オリジナル
2022-12-21 19:17:247585ブラウズ

Go 言語では、配列は特定の型の固定長要素のシーケンスです。同じデータ型の要素のコレクションです。配列は 0 個以上の要素で構成できます。配列に対応する型はスライスです。スライスは配列の連続フラグメントへの参照であるため、スライスは参照型です。このフラグメントは、配列全体、または開始インデックスと終了インデックスによって識別される一部の項目にすることができます。サブセット、終端インデックスによって識別される項目はスライスに含まれないことに注意してください。

Go言語のスライスと配列とは何ですか

このチュートリアルの動作環境: Windows 7 システム、GO バージョン 1.18、Dell G3 コンピューター。

1. 配列

配列は、同じデータ型の要素のコレクションです。 Go 言語では、配列は 宣言された時点で決まります 配列のメンバーは使用中に変更できますが、 配列のサイズは変更できません 基本構文:

// 定义一个长度为3元素类型为int的数组a
var a [3]int

配列の長さは定数である必要があり、

長さは配列型の一部です。一度定義した長さは変更できません

#1. 配列の初期化

(1)方法 1

	var testArray [3]int               // 定义数组时,会初始化int类型为零值
	var cityArray = [3]string{"北京", "上海", "深圳"} // 使用指定的初始值完成初始化

(2) 方法 2

一般に、初期値の数に基づいてコンパイラに配列の長さを推測させることができます

var cityArray = [...]string{"北京", "上海", "深圳"}

(3) メソッド 3

#インデックス値を指定して配列を初期化することもできます。たとえば、

func main() {
	a := [...]int{1: 1, 3: 5}
	fmt.Println(a)                  // [0 1 0 5]
	fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}

#2、配列 Traversal
func main() {
	var a = [...]string{"北京", "上海", "深圳"}
	// 方法1:for循环遍历
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}

	// 方法2:for range遍历
	for index, value := range a {
		fmt.Println(index, value)
	}
}

3. 多次元配列Go 言語は多次元配列をサポートしています。例として 2 次元配列 (配列内にネストされた配列)。

(1) 2次元配列の定義

func main() {
	a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
	fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
	fmt.Println(a[2][1]) //支持索引取值:重庆
}

(2) 2次元配列の走査

func main() {
	a := [3][2]string{
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}
	for _, v1 := range a {
		for _, v2 := range v1 {
			fmt.Printf("%s\t", v2)
		}
		fmt.Println()
	}
}

注: 多次元配列 最初のレベル のみが ... を使用してコンパイラに配列の長さを推定させることができます。例: <pre class="brush:js;toolbar:false;">a := [...][2]string{ {&quot;北京&quot;, &quot;上海&quot;}, {&quot;广州&quot;, &quot;深圳&quot;}, {&quot;成都&quot;, &quot;重庆&quot;}, }</pre>

4. 配列は値型です 配列は値型であり、代入とパラメータの受け渡しはコピーされます配列全体。したがって、

コピーの値を変更しても、それ自体の値は変更されません。

<pre class="brush:js;toolbar:false;">func modifyArray(x [3]int) { x[0] = 100 } func modifyArray2(x [3][2]int) { x[2][0] = 100 } func main() { a := [3]int{10, 20, 30} modifyArray(a) //在modify中修改的是a的副本x fmt.Println(a) //[10 20 30] b := [3][2]int{ {1, 1}, {1, 1}, {1, 1}, } modifyArray2(b) //在modify中修改的是b的副本x fmt.Println(b) //[[1 1] [1 1] [1 1]] }</pre>

注:

配列では、メモリが限られているため、「==」、「!=」演算子がサポートされています。常に初期化されています。
  • #[n]*T
  • はポインターの配列を表します (これは
  • 配列、内部の要素は 1 つずつポインターです)* [n]T
  • は配列ポインタを表します (これは配列のメモリ アドレスを格納する
  • ポインタ です)

two 、スライススライスは、同じ型の要素の 可変長シーケンス

です。これは、配列タイプに基づく

カプセル化層 です。非常に柔軟性が高く、 自動拡張をサポートしています。 スライスは

参照型

であり、その内部構造には addresslengthcapacity# が含まれています##。スライスは通常、データのコレクションを迅速に操作するために使用されます。 スライスは配列の連続フラグメントへの参照であるため、スライスは参照型です (つまり、C/C の配列型、または Python のリスト型に似ています)。これは配列全体であるか、開始インデックスと終了インデックスによって識別される一部の項目のサブセットである可能性があります。終了インデックスによって識別される項目はスライスに含まれないことに注意してください。 Go 言語のスライスの内部構造には、アドレス、サイズ、容量が含まれます。スライスは通常、データ セットをすばやく操作するために使用されます。データ セットをケーキのカットにたとえると、スライスは「ピース」です。切断プロセスには、どこから開始するか (スライスの開始位置)、どのくらいの大きさに切断するか (スライスのサイズ) が含まれます。容量は、スライスを保持するポケットのサイズとして理解できます。



1. スライスの定義

スライス タイプを宣言するための基本的な構文は次のとおりです:
var name []T

// name:表示变量名
// T:表示切片中的元素类型
例:

func main() {
	// 声明切片类型
	var a []string              //声明一个字符串切片
	var b = []int{}             //声明一个整型切片并初始化
	var c = []bool{false, true} //声明一个布尔切片并初始化
	var d = []bool{false, true} //声明一个布尔切片并初始化
	fmt.Println(a == nil)       //true
	fmt.Println(b == nil)       //false
	fmt.Println(c == nil)       //false
	// fmt.Println(c == d)   //切片是引用类型,不支持直接比较,只能和nil比较
}

2. スライスの長さと容量

スライスには独自の長さと容量があります。組み込みの len() 関数を使用し、組み込みの cap() 関数を使用してスライスの容量を確認します。

3. スライス式

スライス式 文字列、配列、配列またはスライスへのポインタからサブ文字を構築します 文字列またはスライス ## #。これには 2 つのバリエーションがあります。1 つはインデックスの下限値と上限値 を指定する単純な形式

で、もう 1 つは

がインデックスの下限値と上限値に加えて容量も指定する完全な形式です。値。フォーム完全なスライス式は役に立ちません。ここでは単純なスライス式についてのみ説明します。

// 简单切片表达式
func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}

実行結果:

s:[2 3] len(s):2 cap(s):4

(1) make() 関数を使用してスライスを構築します

私たちは皆、以下に基づいてスライスを作成しました。配列 スライスを動的に作成する必要がある場合は、組み込みの make() 関数を使用する必要があります。形式は次のとおりです。

make([]T, size, cap)

  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

举个栗子:

func main() {
	a := make([]int, 2, 10)
	fmt.Println(a)      //[0 0]
	fmt.Println(len(a)) //2
	fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

(2)切片的本质

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中

切片的本质 就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)

举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。

Go言語のスライスと配列とは何ですか

切片s2 := a[3:6],相应示意图如下:

Go言語のスライスと配列とは何ですか

如果你懂了切片的本质,那么试试下面这个题吧!

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
	s2 := s[3:4]  // 索引的上限是cap(s)而不是len(s),可能认为cap是2?切片是从原数组中元素2开始切走的
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}

运行结果:

s:[2 3] len(s):2 cap(s):4
s2:[5] len(s2):1 cap(s2):1

s2什么鬼?[2 3][3:4]这个能运行?如果有这样的疑惑,说明你并没有认识到切片的本质,下面我们来看一个图:

注意切片的本质是一个指向底层数组的起点的指针切片len有效长度,以及cap容量

Go言語のスライスと配列とは何ですか

上面是切片s生成的过程,现在又要切片取[3:4],从s的起点开始数,我们可以很容易看出来[3:4]是5。

(3)切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

注意:nil和空不是一个概念,nil的判断是有无底层数组,s2、s3初始化了的,其实是有底层数组的,s1只是声明,因此没有底层数组为nil。是否为空,则len是否为0为唯一判断条件。

(4)切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

func main() {
	s1 := make([]int, 3) //[0 0 0]
	s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100
	fmt.Println(s1) //[100 0 0]
	fmt.Println(s2) //[100 0 0]
}

(5)切片遍历

切片的遍历方式和数组是一致的,支持索引遍历for range遍历。

func main() {
	s := []int{1, 3, 5}

	for i := 0; i < len(s); i++ {
		fmt.Println(i, s[i])
	}

	for index, value := range s {
		fmt.Println(index, value)
	}
}

(6)append()方法为切片添加元素

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

func main(){
	var s []int
	s = append(s, 1)        // [1]
	
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7]
}
// 这个...类似于python中的*args打散列表

注意: 通过var声明的零值切片可以在append()函数直接使用,无需初始化。

var s []int
s = append(s, 1, 2, 3)

没有必要像下面的代码一样初始化一个切片再传入append()函数使用

s := []int{}  // 没有必要初始化
s = append(s, 1, 2, 3)

var s = make([]int)  // 没有必要初始化
s = append(s, 1, 2, 3)

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值

(7)切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

Go言語のスライスと配列とは何ですか

(8) 使用copy()函数复制切片

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(b) //[1 2 3 4 5]
	b[0] = 1000
	fmt.Println(a) //[1000 2 3 4 5]
	fmt.Println(b) //[1000 2 3 4 5]
}

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用方法如下:

func main() {
	// copy()复制切片
	a := []int{1, 2, 3, 4, 5}
	c := make([]int, 5, 5)
	copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	c[0] = 1000
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5] // 再对切片c操作,就不会影响a了
}

(9)从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

func main() {
	// 从切片中删除元素
	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
	// 要删除索引为2的元素
	a = append(a[:2], a[3:]...) // 把index=2之后的切片和index=2之前的切片拼接在一起
	fmt.Println(a) //[30 31 33 34 35 36 37]
}

切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

(10)内存优化

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。

一种解决方法是使用上面的copy函数,根据切片生成一个一模一样的新切片。这样我们可以使用新的切片,原始数组可以被垃圾回收。

package mainimport (
    "fmt")func countries() []string {
    a := []string{1, 2, 3, 4, 5}
    b := a[:len(a)-2]
    c := make([]string, len(b))
    copy(c, b) // 将b的内容copy给c
    return c}func main() {
    d := countries()
    fmt.Println(d)
 }

b := a[:len(a)-2] 创建一个去掉a的尾部 2 个元素的切片 b,在上述程序的 11 行,将 切片b 复制到 切片c。同时在函数的下一行返回 切片c。现在 a 数组可以被垃圾回收, 因为数组a不再被引用。

三、切片与数组的区别

Go 数组与像 C/C++等语言中数组略有不同:

1. Go 中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份。因此,在 Go 中如果将数组作为函数的参数传递的话,那效率就肯定没有传递指针高了。

2. 数组的长度也是类型的一部分,这就说明[10]int和[20]int不是同一种数据类型。并且Go 语言中数组的长度是固定的,且不同长度的数组是不同类型,这样的限制带来不少局限性。

3. 而切片则不同,切片(slice)是一个拥有相同类型元素的可变长序列,可以方便地进行扩容和传递,实际使用时比数组更加灵活,这也正是切片存在的意义。而且切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。

【相关推荐:Go视频教程编程教学

以上がGo言語のスライスと配列とは何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。