【Golang基础篇】——array、slice、指针、map

背景

每一门开发语言的基础都是从数据类型开始学起,Java转成Golang,所以小编的学习之路又从零开始了。Golang和其他开发语言一样分为数据类型分为两种值类型和引用类型,值类型比较简单就是一些基本数据类型,无论是否有过其他语言基础,大概看一下也是可以明白的,所以本文主要介绍Golang的引用类型。

基础

值类型:变量直接存储值,内容通常在栈中分配

引用类型:变量存储的是一个地址,这个地址存储最终的值,内容通常在堆上分配,通过GC回收

从Golang的设计思想上来说,Golang是函数式编程,函数式编程最明显的特点不可变,所以Golang的传参特点值传递,默认传递是值,那怎么处理引用传递呢?主要利用本文讲解的这几种引用类型。

Golang中引用类型:指针、slice(切片)、map、chan,chan和并发编程联系比较紧密,放到后面的并发编程中,主要讲解指针、slice、map

数组

 1. 数组:是同一种数据类型的固定长度的序列。
 2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
 3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
 4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
    for i := 0; i < len(a); i++ {
    }
    for index, v := range a {
    }
 5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
 6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
 7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
 8.指针数组 [n]*T,数组指针 *[n]T。

数组初始化方式

package main

import (
    "fmt"
)
//全局
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}

func main() {
    //局部
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }
    //打印全局
    fmt.Println(arr0, arr1, arr2, str)
    //打印局部
    fmt.Println(a, b, c, d)
}

注意点

1、数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。

package main

import (
    "fmt"
)

func test(x [2]int) {
    fmt.Printf("x: %p\n", &x)
    x[1] = 1000
}

func main() {
    a := [2]int{}
    fmt.Printf("a: %p\n", &a)

    test(a)
    fmt.Println(a)
}

//打印结果
a: 0xc42007c010
x: 0xc42007c030
[0 0]

通过这段代码可以推出数组是值类型,在传输的过程中都是复制,内存地址已经不同。

值拷贝行为会造成性能问题,如何解决这个问题,则引入slice,和指针

slice(切片)

slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
2. 切片的长度可以改变,因此,切片是一个可变的数组。
3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。 
4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
5. 切片的定义:var 变量名 []类型,比如 var str []string  var arr []int。
6. 如果 slice == nil,那么 len、cap 结果都等于 0。

创建切片的方式

 //1.声明切片
   var s1 []int
   if s1 == nil {
      fmt.Println("是空")
   } else {
      fmt.Println("不是空")
   }
   // 2.:=
   s2 := []int{}
   // 3.make()
   var s3 []int = make([]int, 0)

  //make创建具体参数明显
   var slice []type = make([]type, len)
   slice  := make([]type, len)
   slice  := make([]type, len, cap)

   s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
   fmt.Println(s1, len(s1), cap(s1))

   s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
   fmt.Println(s2, len(s2), cap(s2))

   s3 := make([]int, 6) // 省略 cap,相当于 cap = len。

make创建切片的内存分配

 切片初始化

全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end] 
var slice1 []int = arr[:end]        
var slice2 []int = arr[start:]        
var slice3 []int = arr[:] 
var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]        
slice7 := arr[start:]     
slice8 := arr[:]  
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素

数组和切片的内存布局

使用 make 动态创建slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3}
    p := &s[2] // *int, 获取底层数组元素指针。
    *p += 100

    fmt.Println(s)
}

输出结果:

    [0 1 102 3]

用append内置函数操作切片

append :向 slice 尾部添加数据,返回新的 slice 对象。 

package main

import (
    "fmt"
)

func main() {

    s1 := make([]int, 0, 5)
    fmt.Printf("%p\n", &s1)

    s2 := append(s1, 1)
    fmt.Printf("%p\n", &s2)

    fmt.Println(s1, s2)

}

输出结果:

    0xc42000a060
    0xc42000a080
    [] [1]

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满

package main

import (
    "fmt"
)

func main() {

    data := [...]int{0, 1, 2, 3, 4, 10: 0}
    s := data[:2:3]

    s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

    fmt.Println(s, data)         // 重新分配底层数组,与原数组无关。
    fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

}
输出结果
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
    0xc4200160f0 0xc420070060

指针

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。

Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

变量指针

ptr := &v    // v的类型为T
v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

从内存角度看下区别

func main() {
    a := 10
    b := &a
    fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
    fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
    fmt.Println(&b)                    // 0xc00000e018
}

可以看到b的实际存储的是a的变量的内存地址,所以b被称为指针类型,那么如何获取指针类型真正的数据值呢?即指针取值

func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    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

总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

    1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
    2.指针变量的值是指针地址。
    3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

空指针是所有程序员逃不开的困难,Go中何为空指针

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断,判断是为nil即可

new和make的使用

new是一个内置的函数,它的函数签名如下:
func new(Type) *Type

1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

func make(t Type, size ...IntegerType) Type

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

var b map[string]int   // 只是声明变量b是一个map类型的变量 此时b=nil
b = make(map[string]int, 10) //需要make进行初始化后才能使用,否会panic

new和make的区别

  1.二者都是用来做内存分配的。
  2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

map

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

//Go中map定义 
map[KeyType]ValueType

KeyType:表示键的类型。
ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存
make(map[KeyType]ValueType, [cap])
cap是make的容量,非必填,但是我们应该在初始化map的时候就为其指定一个合适的容量

map的基本使用


func main() {
    //填充
    scoreMap := make(map[string]int, 8)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    fmt.Println(scoreMap)
    fmt.Println(scoreMap["小明"])
    fmt.Printf("type of a:%T\n", scoreMap)
    //在声明时填充
    userInfo := map[string]string{
        "username": "pprof.cn",
        "password": "123456",
    }
    
    //判断key是否存在,定义 value, ok := map[key]
    // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
    v, ok := scoreMap["张三"]
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
    
    //map遍历 range
    //同时需要k,v
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
    //只需要k
    for k := range scoreMap {
        fmt.Println(k)
    }

    //删除指定key、delete(map, key)
    //map:表示要删除键值对的map
    //key:表示要删除的键值对的键
    delete(scoreMap, "小明")//将小明:100从map中删除
    
}

总结

array是值类型,但是是引用类型扩展的基石,值类型的拷贝会存在性能问题,所以Go中提供了引用类型,引用类型的使用离不开指针,搞懂指针需要类型,指针地址、指针类型、指针取值即可,&获取地址,*根据地址取出地址指向的值。slice相当于提供了动态可自由扩容的数组,而map则相当于提供了可根据指定值查找value的数组。从简化的角度上来看还是相对简单的,如果有其他语言基础,举一反三效果会更好些。当然在使用它们的过程,还是有些坑点,后续的文章会继续更新slice、map底层实现机制,以及特殊注意点。

PS:Go系列的第一篇终于开始了,Java转Go的我,终于要认真的转Go了,初学者,如有理解错误支持,恳请各位大佬赐教,不胜感激。

  • 1
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页
评论 1

打赏作者

Mandy_i

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值