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

一夕烟云 · · 411 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

值类型和引用类型区别

值类型:使用变量指向内存的值,内存分配通常在栈中,发生赋值和参数传递时是把这个数据的值(可能多个数据,如数组包括数据域和长度)一起拷贝一份。
引用类型:引用类型数据是使用变量的内存地址,或内存地址中第一个字所在的位置,这个内存地址被称之为指针,这个指针实际上也被存在另外的某一个字中。

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

数组

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

定义
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}

image.png

基本操作

//数组下标是从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内存结构:
image.png

定义
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
    }

}

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

本文来自:Segmentfault

感谢作者:一夕烟云

查看原文:Go基础编程:数组、切片、字符串

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

411 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传