Go语言之GO 语言引用类型

Posted heych

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言之GO 语言引用类型相关的知识,希望对你有一定的参考价值。

GO 语言引用类型

Go 语言切片

Go 语言切片(Slice)

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片

申明一个未指定大小的数组来定义切片:

var identifier []type

切片不需要说明长度。

或使用make()函数来创建切片:

var slice1 []type = make([]type, len)

也可以简写为

slice1 := make([]type, len)

也可以指定容量,其中capacity为可选参数。

make([]T, length, capacity)

这里 len 是数组的长度并且也是切片的初始长度。Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

切片初始化

s :=[] int {1,2,3 } 

直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3

s := arr[:] 

初始化切片s,是数组arr的引用

s := arr[startIndex:endIndex] 

将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片

s := arr[startIndex:] 

默认 endIndex 时将表示一直到arr的最后一个元素

s := arr[:endIndex] 

默认 startIndex 时将表示从arr的第一个元素开始

s1 := s[startIndex:endIndex] 

通过切片s初始化切片s1

s :=make([]int,len,cap) 

通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片

len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

以下为具体实例:

func printSlice(x []int) {
	fmt.Printf("len= %d , silce=%v
", len(x), cap(x))

}

func main() {
	var n = make([]int, 3, 5)
	printSlice(n)

}
PS D:goprogramgosrcday05> .funtion.exe
len= 3 , silce=5

空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

func printSlice(x []int) {
	fmt.Printf("len= %d , cap=%d slice=%v 
", len(x), cap(x), x)

}

func main() {
	var n []int
	if n == nil {
		fmt.Println("切片是空的")
	}
	printSlice(n)

}
切片是空的
len= 0 , cap=0 slice=[]

切片截取

可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:

package main

import "fmt"

func main() {
   /* 创建切片 */
   numbers := []int{0,1,2,3,4,5,6,7,8}  
   printSlice(numbers)

   /* 打印原始切片 */
   fmt.Println("numbers ==", numbers)

   /* 打印子切片从索引1(包含) 到索引4(不包含)*/
   fmt.Println("numbers[1:4] ==", numbers[1:4])

   /* 默认下限为 0*/
   fmt.Println("numbers[:3] ==", numbers[:3])

   /* 默认上限为 len(s)*/
   fmt.Println("numbers[4:] ==", numbers[4:])

   numbers1 := make([]int,0,5)
   printSlice(numbers1)

   /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
   number2 := numbers[:2]
   printSlice(number2)

   /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
   number3 := numbers[2:5]
   printSlice(number3)

}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v
",len(x),cap(x),x)
}


len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]

append() 和 copy() 函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v
",len(x),cap(x),x)
}


len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。

切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……,代码如下:

var numbers []int
for i := 0; i < 10; i++ {
    numbers = append(numbers, i)
    fmt.Printf("len: %d  cap: %d pointer: %p
", len(numbers), cap(numbers), numbers)
}
len: 1  cap: 1 pointer: 0xc0420080e8
len: 2  cap: 2 pointer: 0xc042008150
len: 3  cap: 4 pointer: 0xc04200e320
len: 4  cap: 4 pointer: 0xc04200e320
len: 5  cap: 8 pointer: 0xc04200c200
len: 6  cap: 8 pointer: 0xc04200c200
len: 7  cap: 8 pointer: 0xc04200c200
len: 8  cap: 8 pointer: 0xc04200c200
len: 9  cap: 16 pointer: 0xc042074000
len: 10  cap: 16 pointer: 0xc042074000


使用函数 len() 查看切片拥有的元素个数,使用函数 cap() 查看切片的容量情况。
切片长度 len 并不等于切片的容量 cap。

切片删除

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
删除开头的元素可以直接移动数据指针:也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):还可以用 copy() 函数来删除开头的元素:

从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:

a = []int{1, 2, 3, ...}a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
从尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素a = a[:len(a)-N] // 删除尾部N个元素

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况,下面来看一个示例。

代码输出结果:代码说明如下:

  • 第 1 行,声明一个整型切片,保存含有从 a 到 e 的字符串。
  • 第 4 行,为了演示和讲解方便,使用 index 变量保存需要删除的元素位置。
  • 第 7 行,seq[:index] 表示的就是被删除元素的前半部分,值为 [1 2],seq[index+1:] 表示的是被删除元素的后半部分,值为 [4 5]。
  • 第 10 行,使用 append() 函数将两个切片连接起来。
  • 第 12 行,输出连接好的新切片,此时,索引为 2 的元素已经被删除。

代码的删除过程可以使用下图来描述。
技术图片
提示

连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

多维切片

Go语言中同样允许使用多维切片,声明一个多维数组的语法格式如下:

var sliceName [][]...[]sliceType

其中,sliceName 为切片的名字,sliceType为切片的类型,每个[ ]``[ ]
下面以二维切片为例,声明一个二维切片并赋值,代码如下所示。上面的代码也可以简写为下面的样子。

// 声明一个二维整型切片并赋值slice := [][]int{{10}, {100, 200}}

上面的代码中展示了一个包含两个元素的外层切片,同时每个元素包又含一个内层的整型切片,切片 slice 的值如下图所示。
技术图片
图:整型切片的切片的值

通过上图可以看到外层的切片包括两个元素,每个元素都是一个切片,第一个元素中的切片使用单个整数 10 来初始化,第二个元素中的切片包括两个整数,即 100 和 200。
数据结构 append()
【示例】组合切片的切片

// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

Go语言里使用 append() 函数处理追加的方式很简明,先增长切片,再将新的整型切片赋值给外层切片的第一个元素,当上面代码中的操作完成后,再将切片复制到外层切片的索引为 0 的元素,如下图所示。
技术图片

图:append 操作之后外层切片索引为 0 的元素的布局

Go 语言容器(Map)

Go语言Map(集合)

Map是一种无序的键值对的集合。Map最重要的一点是通过key来快速检索数据,key索引索引,指向数据的值。

不过,地图是无序的,我们无法决定它的返回顺序,这是因为地图是一种哈希表来实现的。

定义地图

可以使用内建函数make也可以使用map关键字来定义Map:

/ *声明变量,默认map是nil * /
var map_variable map [key_data_type] value_data_type

/ *使用make函数* /
map_variable:= make(map [key [data_data_type] value_data_type)

如果不初始化map,那么就会创建一个nilmap。nilmap不能放入放置键值对

package main

import "fmt"

func main() {
	// 定义集合map的方式
	// 第一种
	var c map[string]string   //声明集合类型
	c = map[string]string{"name": "张三", "age": "20"}  //创建map并初始化
	for i := range c {
		fmt.Println(i, c[i])
	}
	// 第二种方式
	b := make(map[string]string)
	b["中国"] = "北京"
	for i := range b {
		fmt.Println(i, b[i])
	}
}

map 容量

和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
例如:

make(map[keytype]valuetype, cap)
map2 := make(map[string]float, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

用切片作为 map 的值

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

delete() 函数

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。

package main

import "fmt"

func main() {
        /* 创建map */
        countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

        fmt.Println("原始地图")

        /* 打印地图 */
        for country := range countryCapitalMap {
                fmt.Println(country, "首都是", countryCapitalMap [ country ])
        }

        /*删除元素*/ delete(countryCapitalMap, "France")
        fmt.Println("法国条目被删除")

        fmt.Println("删除元素后地图")

        /*打印地图*/
        for country := range countryCapitalMap {
                fmt.Println(country, "首都是", countryCapitalMap [ country ])
        }
}

原始地图
India 首都是 New delhiIndia 首都是 New delhi
France 首都是 ParisFrance 首都是 Paris
Italy 首都是 RomeItaly 首都是 Rome
Japan 首都是 TokyoJapan 首都是 Tokyo
法国条目被删除法国条目被删除
删除元素后地图删除元素后地图
Italy 首都是 RomeItaly 首都是 Rome
Japan 首都是 TokyoJapan 首都是 Tokyo
India 首都是 New delhiIndia 首都是 New delhi

获取长度

fmt.Println(len(mymap))    cap无效

判断key是否存在

value, ok := map[key]
检测一个特定的键是否存在于 map 中。如果 ok 是true,则键存在,value 被赋值为对应的值。如果 ok 为 false,则表示键不存在。

value, ok := mymap["name"]
    fmt.Println(value, ok) //张三 true

遍历

range for 可用于遍历 map 中所有的元素

    for k, v := range mymap { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。
        println(k, v)
    }

从 map 中取回的是?个 value 临时复制品,对其成员的修改是没有任何意义的。
可以在迭代时安全删除键值。

for k, v := range mymap { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。
        println(k, v)
        delete(mymap,k)
    }

比较

比较 map,map 不能通过 == 操作符比较是否相等。== 操作符只能用来检测map 是否为 nil

//判断两个map是否相等
func comparisonMap(map1 ,map2 map[string]string) bool {
    //判断长度
    if len(map1) != len(map2) {
        return false
    }
    //判断值
    for key, value := range map1 {
        if value !=map2[key] {
            return false
        }
    }
    return true
}

sync.Map

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

// 创建一个int到int的映射
m := make(map[int]int)
// 开启一段并发代码
go func() {
    // 不停地对map进行写入
    for {
        m[1] = 1
    }
}()
// 开启一段并发代码
go func() {
    // 不停地对map进行读取
    for {
        _ = m[1]
    }
}()
// 无限循环, 让并发程序在后台执行
for {
}

运行代码会报错,输出如下:

fatal error: concurrent map read and map write

错误信息显示,并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现。
需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。

并发安全的 sync.Map 演示代码如下:

package main
import (
      "fmt"
      "sync"
)
func main() {
    var scene sync.Map
    // 将键值对保存到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)
    // 从sync.Map中根据键取值
    fmt.Println(scene.Load("london"))
    // 根据键删除对应的键值对
    scene.Delete("london")
    // 遍历所有sync.Map中的键值对
    scene.Range(func(k, v interface{}) bool {
        fmt.Println("iterate:", k, v)
        return true
    })
}
100 true
iterate: egypt 200
iterate: greece 97

代码说明如下:

  • 第 10 行,声明 scene,类型为 sync.Map,注意,sync.Map 不能使用 make 创建。
  • 第 13~15 行,将一系列键值对保存到 sync.Map 中,sync.Map 将键和值以 interface{} 类型进行保存。
  • 第 18 行,提供一个 sync.Map 的键给 scene.Load() 方法后将查询到键对应的值返回。
  • 第 21 行,sync.Map 的 Delete 可以使用指定的键将对应的键值对删除。
  • 第 24 行,Range() 方法可以遍历 sync.Map,遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回。

sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

Go 语言管道

GO 语言管道

Channel概念

Channel 是Go中的一个核心类型,你可以把它看成一个管道。Channel是引用类型,操作符是箭头 <-
Channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
Channel 是线程安全的,先进先出,多个goroutine同时访问,不需要加锁,channel是有类型的,一个整数的channel只能存放整数。

Channel定义

第一种,channel声明

声明int类型的chan

package main

func main() {
	var ch chan int
}

声明string类型的chan

var ch chan string

声明map类型chan

var ch chan map[int]string

第二种,使用make定义,无缓冲channel

package main

func main() {
	var ch1 chan int = make(chan int)
	ch2 := make(chan int)
	var ch3 = make(chan int)
}

第三种,使用make定义,有缓冲channel

var ch1 chan int = make(chan int, 10)
var ch2 = make(chan int, 10)
ch3 := make(chan int, 10)

第四种,只读channel

var ch1 <-chan int
var ch2 <-chan int = make(<-chan int, 10)
var ch3 = make(<-chan int, 10)
ch4 := make(<-chan int, 10)

第五种,只写channel

var ch1 chan<- int
var ch2 chan<- int = make(chan<- int, 10)
var ch3 = make(chan<- int, 10)
ch4 := make(chan<- int, 10)

Channel特点

无缓冲的与有缓冲channel有着重大差别,那就是一个是同步的 一个是非同步的。

比如

无缓冲chan:ch1:=make(chan int)

有缓冲chan:ch2:=make(chan int,1)

无缓冲: ch1<-1 不仅仅是向 c1 通道放 1,而是一直要等有别的携程 <-ch1 接手了这个参数,那么ch1<-1才会继续下去,要不然就一直阻塞着。

有缓冲: ch2<-1 则不会阻塞,因为缓冲大小是1(其实是缓冲大小为0),只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。

缓冲区是内部属性,并非类型构成要素。

普通 channel 可以隐式转为只读channel或只写channel。

package main

func main() {
	var ch = make(chan int, 3)
	var send chan<- int = ch //写
	var recv <-chan int = ch //读

}

只读channel或只写channel不能转为普通 channel。

package main

func main() {
    var send chan<- int
    var recv <-chan int

    ch1 := (chan int)(send)
    ch2 := (chan int)(recv)
}

编译错误:

./main.go:7:19: cannot convert send (type chan<- int) to type chan int
./main.go:8:19: cannot convert recv (type <-chan int) to type chan int

Channel操作

使用内置函数 len() 返回未被读取的缓冲元素数量,使用内置函数 cap() 返回缓冲区大小。

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int, 3)
	ch2 <- 1
	fmt.Println(len(ch1))
	fmt.Println(len(ch2))
}


D:goprogramgosrc管道
λ go run test.go
ch1 缓冲元素数量:0,缓冲区大小:0
ch2 缓冲元素数量:1,缓冲区大小:3

channel 写入、读取操作:

package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	ch <- 99

	value, ok := <-ch
	if ok {
		fmt.Printf("读取chan:%v
", value)
	}

}

                         
D:goprogramgosrc管道   
λ go run test.go         
读取chan:99                

channel 关闭操作:

1、使用内置函数 close() 进行关闭 chan。
2、chan关闭之后,for range遍历chan中已经存在的元素后结束。
3、没有使用for range的写法需要使用,val, ok := <- ch进行判断chan是否关闭。

package main

import "fmt"

func main() {
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)
	for {
		val, ok := <-ch
		if ok == false {
			fmt.Println("chan is closed")
			break
		}
		fmt.Println(val)
	}

}

                           
D:goprogramgosrc管道     
λ go run test.go           
1                          
2                          
3                          
chan is closed             
                           

注意:向已经关闭的 channel 发送数据会引发 panic 错误。

package main

func main() {
	ch := make(chan int, 5)
	close(ch)
	ch <- 100

}

D:goprogramgosrc管道
λ go run test.go
panic: send on closed channel

goroutine 1 [running]:
main.main()
        D:/goprogram/go/src/管道/test.go:6 +0x6a
exit status 2

以上是关于Go语言之GO 语言引用类型的主要内容,如果未能解决你的问题,请参考以下文章

7Go语言基础之切片(slice)

Go语言基础之map

go语言之指针

go语言之指针

《Go语言实战》Go 类型:基本类型引用类型结构类型自定义类型

Go语言基础之map