很多从php转到golang的开发者,一直搞不清楚golang中的数组、slice切片、map映射,本文一次性从基础知识帮大家搞清楚他们。

目录

数组

  • 数组

    golang的数组不同于php。php数组的长度是动态的,数组赋值、传参默认都是引用赋值、传参,不是另开辟内存空间。

    在声明定义的时候,长度就固定了,而且值类型必须一致。数组是值类型,不是引用类型,就是说赋值和传参都是复制整个数组另开内存空间。

    元素item使用{}包起来

    var a [4]int = [4]int{1,2,3} //不足4个元素,用int默认值0补足
    var b = [...]int{1,2,3,4} //[...]表示初始化时确定长度
    var c = [...]int{0:1,1:2,2:3} //支持索引
    var dd = [...][2]int{{1,2,3},{3,2,1}} //多维数组   
    var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}} //第二维不能使用...
    var d = [...]struct {   //数组元素是结构体
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }
    

    注意:数组是采用值拷贝,而值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。

  • 数组传参

    前面说数组是值类型的,传参时默认也是值类型传参;

    我们通常通过&来实现数组引用传参(除非一定要求值类型传参):

    package main
    
    import "fmt"
    
    func sumArr(a [5]int) int {
        var sum int = 0
        for i := 0; i < len(a); i++ {
            sum += a[i]
        }
        return sum
    }
    func printArr(arr *[5]int) {
        arr[0] = 10
        for i, v := range arr {
            fmt.Println(i, v)
        }
    }
    
    func main() {        
        var arr1 [5]int
        sum := sumArr(arr1) //值类型
        printArr(&arr1) //引用类型,传址
        fmt.Println(arr1)
        arr2 := [...]int{2, 4, 6, 8, 10}
        printArr(&arr2)
        fmt.Println(arr2)
    }
    

    注意:& 和 *,可以解释成传址和取值(根据地址取值)。在c、php也通过&和*实现取址与取值。

    是不是觉得golang的数组也太麻烦了,php的数组简单,正是因为php的解释型和动态性,使得php数组强大且编码简单,在性能上比不上golang在编译阶段就确定类型和长度的数组执行效率。

指针

前面数组中提到取址与取值,我们就先了解下golang的指针。

指针,从学编程开始,很多地方都在强调指针多重要,也不好理解,有常用到指针的语言通常都是比较难以精通的语言

指针是什么?指针就是内存地址。

在golang中,不能进行指针偏移和运算,属于安全性指针

两个符号:

  • & :变量取址
  • * :指针取值

三个概念:指针地址、指针类型、指针取值

  • 指针地址:指针,变量的内存地址
  • 指针类型:每个变量类型都对应一个指针类型,比如*string,*[4]int
  • 指针取值:通过内存地址取得地址存储的值(也是变量的值,因为变量的内存地址就是指针)
func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中,也就是变量b指向变量a指向的内存地址
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

输出:

type of b:*int
type of c:int
value of c:10

Slice切片

  • slice切片

    slice表面上是数组的子集,但slice其实是引用类型,通过内部指针和相关属性引用数组片段实现

    slice的语法和python的列表切片语法是类似的

    var a = [10]int{1,2,3,4,5}
    var slice1 = a[1:3]
    var slice2 = [:]
    

    slice同样适合使用len、cap等数组可使用的函数方法,但受限于数组,比如长度不可能超出原数组

    更多数组切片操作:

    s[n]    切片s中的索引位置为n的元素
    s[:]
    s[low:]
    s[:high]
    s[low:high]
    s[low:high:max] 从切片s的索引位置low到high获得切片len=high-lowcap=max-low
    len(s)  切片长度
    cap(s)  切片容量
    
    data := [...]int{0, 1, 2, 3, 4, 5}
    
    s := data[2:4]
    s[0] += 100
    s[1] += 200
    
    fmt.Println(s)
    fmt.Println(data)
    

    输出:

    [102 203]
    [0 1 102 203 4 5]
    

    注意:切片采用的是左闭右开方式,即包含索引low但不包含索引high的元素,也是大家说的前包后不包;切片赋值默认是引用类型,所以更改了切片元素时,也会更改原数组的元素值

    参考切片内存地址:

    回过头来看php的数组,对开发者太友好了,不需要记忆太多语法糖,学习成本低,看起来符合自然易理解,不用关心固定不固定长度,开发者想怎么处理数组就怎么处理,不用担心是不是会出错,出错的概率太低了,php的数组兼容性太强,表达出了简约不简单的理念。

  • make直接创建切片

    make是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

    make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

    make可以创建切片

    var slice []type = make([]type, len, cap)
    
    slice := make([]int,0,5) //创建切片:类型int、初始长度0、容量5
    

    除了make可以创建切片外,还可以通过初始化表达式构造:

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

    直接创建切片,底层会自动创建数组。也就是说切片都是基于数组基础上

  • 二维切片

    data := [][]int{
        []int{1, 2, 3},
        []int{100, 200},
        []int{11, 22, 33, 44},
    }
    fmt.Println(data)
    
    
    [[1 2 3] [100 200] [11 22 33 44]]
    

    php开发者看到这里估计有点不耐烦了,为什么这么麻烦呢,哈哈

    //php一个动态数组就这样定义,只因为类型不要求,甚至可以类型混合在一起,还支持键值对。确实php给开发者的心里负担少了很多,不用关心太多类型、长度等问题,只要关心业务逻辑问题,当然php赢了空间输了时间。
    $data = [
        [1,2,3],
        [100,200],
        [11,22,33,44],
        ["a",'b',3],
        ["a"=>"apple",2]
    ];
    var_dump($data);
    
  • append

    切片追加

    var a = []int{1, 2, 3}
    fmt.Printf("slice a : %v\n", a)
    var b = []int{4, 5, 6}
    fmt.Printf("slice b : %v\n", b)
    c := append(a, b...)
    fmt.Printf("slice c : %v\n", c)
    d := append(c, 7)
    fmt.Printf("slice d : %v\n", d)
    e := append(d, 8, 9, 10)
    fmt.Printf("slice e : %v\n", e)
    
    

    注意:如果append后超出原切片的cap容量,将复制数据并分配一个新数组(重新分配地址,即切片与原数组已经不再引用同一个地址)

  • copy

    切片复制,函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

  • 遍历

    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    slice := data[:]
    for index, value := range slice {
        fmt.Printf("inde : %v , value : %v\n", index, value)
    }
    
  • 字符串与切片

    string在底层是一个byte的数组,也支持切片操作

    对于静态编译语言,string定义后是不可变的,要修改字符,需要转换成[]byte(str)或[]rune(str)处理后,再强制转换成string。rune用于处理中文字符串,前面已经提及

    对于php动态脚本语言,string变量运行时是动态可变的,牺牲性能与空间来不麻烦php开发人员

map映射

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用

  • 定义map

    语法:make(map[KeyType]ValueType, [cap])

    func main() {
        scoreMap := make(map[string]int, 8){
            "小红":99
        }
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        fmt.Println(scoreMap)
        fmt.Println(scoreMap["小明"])
        fmt.Printf("type of a:%T\n", scoreMap)
    }
    

    看了这个示例,有php开发者可能又要说php数组了,php数组可以是列表、对象、结构体,动态混合可调整,还能互相转换,不考虑编译/解释、空间效率问题,让php开发者会觉得很轻松。强类型语言就是要求变量定义的时候尽量明确类型、大小,因为在编译的时候,尽量要知道变量的类型及要分配的空间大小,减少运行时的检查与转换,提高运行时的效率。

  • 判断键值对是否存在

    func main() {
        scoreMap := make(map[string]int)
        scoreMap["小红"] = 90
        scoreMap["小明"] = 100
        // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
        v, ok := scoreMap["小红"]
        if ok {
            fmt.Println(v)
        } else {
            fmt.Println("找不到")
        }
    }
    
  • 遍历

    采用迭代器range进行遍历,map遍历是无序的

  • 删除

    delete(map,key)