20

Go基础编程:数组、切片、字符串

 3 years ago
source link: https://studygolang.com/articles/31541
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

值类型和引用类型区别

值类型:使用变量指向 内存的值 ,内存分配通常在栈中,发生赋值和参数传递时是把这个数据的值(可能多个数据,如数组包括数据域和长度)一起拷贝一份。

引用类型:引用类型数据是使用变量的 内存地址 ,或内存地址中第一个字所在的位置,这个内存地址被称之为指针,这个指针实际上也被存在另外的某一个字中。

为什么要把数组、切片、字符串三种类型一起讲呢?因为它们的数据结构具有紧密联系,在底层他们的内存结构是一样的,只是他们在上层因为语法的原因表现不一样。数组和字符串是值类型,切片是引用类型。

数组

数组是由 同一种数据 元素组成 固定 长度的序列,一个数组由零个或多个元素组成。数组是值类型数据,长度不可改变,内容可变,数组本身赋值和传递参数都是整体复制。因为数组是固定长度,往往使用比较少更多使用的是切片。

定义

var a [4]int                   //定义长度4,元素类型为int,值为  0 0 0 0
var b = [5]int{1, 2, 3, 4}     //定义长度5,元素类型为int,值为  1 2 3 4 0
var c = [...]int{1, 2, 3, 4}   //定义长度4,元素类型为int,值为  1 2 3 4
var d = [4]int{1: 2, 3}        //定义长度4,元素类型为int,值为  0 2 3 0
var e [4]int = [4]int{1: 2, 3} //定义长度4,元素类型为int,值为  0 2 3 0
var f  = [2]string{"hello","world"} //定义长度2,元素类型为string,值为  "hello","world"

内存结构,下面是数组b的内存结构,比较简单:

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

n2uiYrr.png!mobile

基本操作

//数组下标是从0开始的。如果下标在数组合法范围之外,则触发访问越界,会panic
var arr = [5]int{1, 2, 3, 4} 
fmt.Println(arr)       //打印全部 [1 2 3 4 0]
fmt.Println(arr[1])  //打印下标为1(第二个元素) 2
fmt.Println(arr[5])  //invalid array index 5 (out of bounds for 5-element array)

for 循环来迭代数组

var arr = [5]int{1, 2, 3, 4}
for range arr {
    fmt.Println("hello world")
}
//1、hello hello hello hello hello 

for k := range arr {
    fmt.Println(k)
}
//2、打印下标:
//0 1 2 3 4

for k, v := range arr {
    fmt.Println(k, v)
}
//3、key=0,val=1 key=1,val=2 key=2,val=3 key=3,val=4 key=4,val=0
//不想打印k,可以用 "_" 代替k,表示忽略k

for i := 0; i < len(arr); i++ {
    fmt.Println(i, arr[i])
}
//4、key=0,val=1 key=1,val=2 key=2,val=3 key=3,val=4 key=4,val=0

两个数组之间的操作

数组是值类型数据,变量指向内存所有数据,包括长度。即数组长度也是数组一部分,故不同长度的数组不能直接赋值,大小也不能比较,相同长度没问题。

//数组长度相同时:
var a = [5]int{1, 2, 3, 4}
var b = [5]int{}
b = a
fmt.Println(b) //[1 2 3 4 0]
fmt.Println(b != a) //false

//数组长度不相同时:
var a = [5]int{1, 2, 3, 4}
var b = [6]int{}
b = a //cannot use a (type [5]int) as type [6]int in assignment
fmt.Println(b != a) //invalid operation: b != a (mismatched types [6]int and [5]int)

//想把a的值给b只能通过下标一个个改
for i := 0; i < len(a); i++ {
    b[i] = a[i]
}
fmt.Println(b) //[1 2 3 4 0 0]

多维数组

多维数组是数组的数组,可把里面的数组成一个我们常使用的元素类型,就好理解了。

声明二维数组声明表达式 : var v_name [行][列]v_type

var a = [3][2]int{{}, {}, {}} //[[0 0] [0 0] [0 0]]
var b = [3][2]int{[2]int{}, [2]int{}, [2]int{}} //[[0 0] [0 0] [0 0]]
var c = [3][2]int{{1}, {2}, {3}} //[[1 0] [2 0] [3 0]]
var d = [3][2]int{{1, 2}, {2, 2}, {3, 2}} //[[1 2] [2 2] [3 2]]

操作

赋值
var arr [3][2]int
fmt.Println(arr) //[[0 0] [0 0] [0 0]]
arr[0][1] = 2
arr[2][0] = 3
fmt.Println(arr) //[[0 2] [0 0] [3 0]]

对应二维数组来说,把每行看成一个元素后就是一个一维数组了,基本操作和一维数组区别不大,其他多维数组也类似,就不一一说了。

字符串

字符串(string)是UTF-8的字符一个序列(字符为ASCII表里长度1字节,其他2~4个字节,如中文3个字节),它是个不可更改的,只读序列,可以包含任意字符。即创建后就不可更改,实质上就是一个定长的字节数组。

Go字符串在底层 reflect.StringHeader 结构:

type StringHeader struct {
   Data uintptr
   Len int
}

字符串组成包括两部分:一部分是指向底层字节数组的指针,一部分是字节长度。字符串在拷贝其实是复制一份 reflect.StringHeader 结构体,并不会复制底层的字节数组。

下面是字符串 hello world 内存结构:

E3mqUfi.png!mobile

定义

Go使用双引号 "" 或反引号 ` 包起来的表示是字符串内容,注意不能用单引号 ' ,单引号表示的是字节(byte),即UTF-8对应的编码

var str string
var str1 = "hello"
var str2 string = "world"
str3 := "hello world"
fmt.Println(str) //
fmt.Println(str1) //hello
fmt.Println(str2)//world
fmt.Println(str3)//hello world

初始值

Go字符串的初始值为空字符串 "" ,注意 " " 不是空字符串,这个表示空格字符串,占一个字节,对应ASCLL码的00100000B(32H)。

特殊字符

有些字符用 \ 加常见的字符表示特殊含义的字符,如换行

  • \n:换行符
  • \r:回车符
  • \t:tab 键
  • \u 或 U:Unicode 字符
  • \\:反斜杠自身

遍历

因为底层是数组,可用 for range 来遍历,需要注意的是每个字符占的大小不一定一样,key 不一定是连续的,如:

str := "中国abc"
for k, v := range str {
    fmt.Println(k, v)
}
//0 20013
//3 22269
//6 97
//7 98
//8 99

不可变性

这个不可变性很容易让人认为这个字符串像常量那样,一旦声明整个生命周期都不能变。这个不能变是通过键下标修改:

str := "abcdef"
fmt.Println(str[1]) //通过下标访问(读): 98
str[1] = "h" // 通过下标改(写):cannot assign to str[1]

可能会疑惑明明可以改变字符串:

str := "abcdef"
fmt.Println(str) //abcdef
str = "hello world"
fmt.Println(str) //hello world

这种改变是把底层指向数组的指针改变的,是重新开辟一个内存地址保存新数据,抛弃掉原指针数组。

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    str := "hello "
    fmt.Println(stringAddr(str)) //4999457
    str = "world"
    fmt.Println(stringAddr(str)) //4999111
}

func stringAddr(s string) uintptr {
    return (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
}

拼接字符串

  1. + 号拼接,会产生开销

    str1 := "hello "
    str2 := "world"
    str := str1 + str2
    fmt.Println(str) //hello world
  2. fmt包下的打印函数,效率不高

    str1 := "hello "
    str2 := "world"
    str := fmt.Sprintf("%s%s", str1, str2)
    fmt.Println(str) //hello world
  3. strings.Join 表示以什么字符串拼接字符串数组,在已有一个数组的情况下,这种效率会很高,如果没有的话效率也不高。

    str1 := "hello "
    str2 := "world"
    str3 := []string{str1, str2}
    str := strings.Join(str3, "")
    fmt.Println(str) //hello world
  4. 字节缓冲 bytes.Buffer 这种方法的性能就要大大优于上面的了

    str1 := "hello "
    str2 := "world"
    var by bytes.Buffer
    by.WriteString(str1)
    by.WriteString(str2)
    fmt.Println(by.String())
  5. strings.Builder 性能和 bytes.Buffer 一样

    str1 := "hello "
    str2 := "world"
    var by strings.Builder
    by.WriteString(str1)
    by.WriteString(str2)
    fmt.Println(by.String())

虽然 bytes.Bufferstrings.Builder 性能最好,官方也建议,但 + 更简单方便,往往使用得最多。

长度

虽然底层结构体有长度,但字符串不包括长度,要获取字符串长度和数组、切片一样用内置函数 len() 获取。字符串的范围是根据长度决定的,而非特殊字符 \0

切片

切片可以看成 动态数组长度不定 ,所以长度不是类型组成部分。切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++中的数组类型,或者 Python中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

Go字符串在底层 reflect.SliceHeader 结构:

type SliceHeader struct {
   Data uintptr
   Len int
   Cap int
}

reflect.SliceHeader 结构体组成有三部分:一部分是切片数据指针,另一部分是切片长度,最后一部分是切片容量。

内存结构

定义

var arr1 []int // nil切片
var arr2 []int = []int{1, 2}// 初始值为[1 2]
var arr3 = []int{1, 2, 3}//初始值为[1 2 3]
var arr4 = arr3[1:]//arr3的部分 初始值为[2 3]
var arr5 = make([]int, 3) // 有3个元素的切片, len和cap都为3
var arr6 = make([]int, 2, 3)// 有2个元素的切片, len为2, cap为3

初始值

初始值为 nil ,不能等价于空字符串 "" 、布尔值 false 和数值上的 0 ,这些都是不同类型数据,不能相互之间转换。

基本操作

切片是动态数组,数组的操作,切片都能。

获取长度、容量

长度是切片的实际长度,容量是切片的最大容量。 0 <= len(s) <= cap(s) 。当向切片添加数据超过切片容量时,会丢掉原切片数据部分,重新开一个空间来存储新数据,开辟空间大小是上一个切片最大容量的2倍。

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    var arr1 = []int{1, 2, 3}
    var arr2 = make([]int, 2, 3)
    fmt.Println(len(arr2), cap(arr2)) //2 3
    fmt.Println(sliceAddr(arr1))      //824634252976
    fmt.Println(len(arr1), cap(arr1)) //3 3
    arr1 = append(arr1, 4)
    fmt.Println(len(arr1), cap(arr1)) //4 6
    fmt.Println(sliceAddr(arr1))      //824634253360
}
//获取切片数据地址
func sliceAddr(s []int) uintptr {
    return (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data
}

单个元素操作

package main

import (
    "fmt"
)

func main() {

    var arr = []int{1, 2, 3}
    //打印下标为1的元素:
    fmt.Println(arr[1]) // 2
    //获取全部元素:
    fmt.Println(arr) //[1 2 3]

    // 更改下标为2的元素
    arr[2] = 5
    fmt.Println(arr[2]) //5

    //不能越界访问
    fmt.Println(arr[3]) //panic: runtime error: index out of range

}

添加操作

内置函数 append() 表示向切片末尾添加元素的意思:

package main

import (
    "fmt"
)

func main() {

    var arr = []int{1, 2, 3}
    fmt.Println(arr) //[1 2 3]
    // 新增一个元素
    arr = append(arr, 4)
    fmt.Println(arr) //[1 2 3 4]
    // 新增2个元素
    arr = append(arr, 5, 6)
    fmt.Println(arr) //[1 2 3 4 5 6]
    // 新增多个个元素,语法糖(arr1...) 表示把arr1 从头到尾一样一一赋值
    var arr1 = []int{20, 30}
    arr = append(arr, arr1...)
    fmt.Println(arr) //[1 2 3 4 5 6 20 30]

}

拷贝

浅拷贝:拷贝内存地址,修改数据对原数据有影响

深拷贝:拷贝这个值,内存地址不一样,修改数据对原数据不影响

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {

    var arr = []int{1, 2, 3, 4, 5, 6, 20, 30}
    fmt.Println(arr) //[1 2 3 4 5 6 20 30]
-------------------------浅拷贝----------------------------
    // 赋值
    arr1 := arr
    fmt.Println(arr1) //[1 2 3 4 5 6 20 30]
    // 截取从下标2及后面的部分
    arr1 = arr[2:]
    fmt.Println(arr1) //[3 4 5 6 20 30]
    // 截取从下标5前面的部分
    arr1 = arr[:5]
    fmt.Println(arr1) //[1 2 3 4 5]
    // 截取从下标2-5的部分
    arr1 = arr[2:5]
    fmt.Println(arr1) //[3 4 5]

    // 切片是引用型数据,这样截取数据实质上是数据指针指向内存地址的改变
    // 故其它在这内存地址上的切片改变,原切片的数据会改变,如下:
    arr1[1] = 88
    fmt.Println(arr) //[1 2 3 88 5 6 20 30]
    
-------------------------深拷贝----------------------------
    // 如果切片本来是引用另一个切片,但元素增加超过切片容量后,会从新开辟空间,对原数据不影响(指向数据的指针不同)
    var arr2 = []int{1, 2, 3}
    fmt.Println(arr2)            //[1 2 3]
    fmt.Println(sliceAddr(arr2)) //原切片数据指针 824633771136
    arr3 := arr2
    fmt.Println(arr3)            //[1 2 3]
    fmt.Println(sliceAddr(arr3)) //新切片数据指针 824633771136
    arr3 = append(arr3, 4, 5, 6) //新增元素超过容量,重新开辟空间
    fmt.Println(arr2)            //[1 2 3]
    fmt.Println(arr3)            //[1 2 3 4 5 6]
    fmt.Println(sliceAddr(arr2)) //原切片数据指针 824633771136
    fmt.Println(sliceAddr(arr3)) //超出容量后,新开辟空间切片数据指针 824633787232

    // 使用内置函数copy(),拷贝一份数据,这种修改数据也对原数据没影响
    // copy()和切片扩容后,都是对原数据切片整个复制,不存在对原数据内存地址的引用,这种叫深拷贝,修改对原数据影响没半毛钱关系
    var arr4 = make([]int, len(arr))
    copy(arr4, arr)
    arr4[1] = 100
    fmt.Println(arr)  //[1 2 3 88 5 6 20 30]
    fmt.Println(arr4) //[1 100 3 88 5 6 20 30]

}

func sliceAddr(s []int) uintptr {
    return (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data
}

遍历

package main

import (
    "fmt"
)

func main() {
    var arr = []int{1, 2, 3, 4, 5, 6, 20, 30}
    for range arr {
        fmt.Printf("%s ", "a") // a a a a a a a a 
    }
    fmt.Println("")
    for k := range arr {
        fmt.Printf("%d ", arr[k]) //1 2 3 4 5 6 20 30 
    }
    fmt.Println("")

    for k, v := range arr {
        fmt.Printf("%d=>%d ", k, v) //0=>1 1=>2 2=>3 3=>4 4=>5 5=>6 6=>20 7=>30 
    }
    fmt.Println("")
    for i := 0; i < len(arr); i++ {
        fmt.Printf("%d ", arr[i]) //1 2 3 4 5 6 20 30
    }

}

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK