手记

go语言学习笔记(五)

一、go语言字典类型

Go语言的字典(Map)类型其实是哈希表(Hash Table)的一个实现。

字典用于存储键-元素对(更通俗的说法是键-值对)的无序集合。

注意,同一个字典中的每个键都是唯一的。

如果我们在向字典中放入一个键值对的时候其中已经有相同的键的话,那么与此键关联的那个值会被新值替换。

字典类型的字面量如下:

map[K]T

    其中,“K”意为键的类型,而“T”则代表元素(或称值)的类型。

如果我们要描述一个键类型为int、值类型为string的字典类型的话,应该这样写:map[int]string

    请注意,字典的键类型必须是可比较的,否则会引起错误。

键类型不能是切片、字典或函数类型。

    字典值的字面量表示法实际上与数组和切片的字面量表示法很相似。

首先,最左边仍然是类型字面量,右边紧挨着由花括号包裹且有英文逗号分隔的键值对。

每个键值对的键和值之间由英文冒号分隔。

以字典类型map[int]string为例,它的值的字面量可以是这样的:

map[int]string{1: "a", 2: "b", 3: "c"}

我们可以把这个值赋给一个变量:

mm := map[int]string{1: "a", 2: "b", 3: "c"}

然后运用索引表达式取出字典中的值,就像这样:

b := mm[2]

注意,在这里,我们放入方括号中的不再是索引值(实际上,字典中的键值对也没有索引),而是与我们要取出的值对应的那个键。在上例中变量b的值必是字符串"b"。

当然,也可以利用索引表达式来赋值,比如这样:

mm[2] = b + "2"

这使得字典mm中与键2对应的值变为了"b2"。

现在我们再来向mm添加一个键值对:

mm[4] = ""

    之后,在从中取出与`4`和`5`对应的值:

d := mm[4]

e := mm[5]

    此时,变量d和e的值都会是多少呢?答案是都为"",即空字符串。

对于变量d来说,由于在字典mm中与4对应的值就是"",所以索引表达式mm[4]的求值结果必为""。

但是mm[5]的求值结果为什么也是空字符串呢?

原因是,在Go语言中有这样一项规定,即:对于字典值来说,如果其中不存在索引表达式欲取出的键值对,那么就以它的值类型的空值(或称默认值)作为该索引表达式的求值结果。由于字符串类型的空值为"",所以mm[5]的求值结果即为""。

       在不知道mm的确切值的情况下,我们无法得知mm[5]的求值结果意味着什么?

它意味着5对应的值就是一个空字符串?还是说mm中根本就没有键为5的键值对?这无所判别。

为了解决这个问题,Go语言为我们提供了另外一个写法,即:

e, ok := mm[5]

    针对字典的索引表达式可以有两个求值结果。第二个求值结果是bool类型的。

它用于表明字典值中是否存在指定的键值对。

在上例中,变量ok必为false。因为mm中不存在以5为键的键值对。

    从字典中删除键值对的方法是调用内建函数delete函数,就像这样:

delete(mm, 4)无论mm中是否存在以4为键的键值对,delete都会“无声”地执行完毕。我们用“有则删除,无则不做”可以很好地概括它的行为。

最后,与切片类型相同,字典类型属于引用类型。它的零值即为nil

实例代码 go_zidian.go:

package main

import "fmt"

func main() {
	//定义一个字典类型:键是string类型,值是int类型
	mm2 := map[string]int{"golang": 42, "java": 1, "python": 8}
	//向字典类型中添加值
	mm2["scala"] = 25
	mm2["erlang"] = 50

	str := mm2["java"]
	fmt.Printf("字典类型键为Java的值是:%d\n", str)

	//删除字典类型中键为python的值
	delete(mm2, "python")

	fmt.Printf("%d, %d, %d \n", mm2["scala"], mm2["erlang"], mm2["python"])

	//遍历字典类型
	for i := 0; i < len(mm2); i++ {
		fmt.Printf("字典类型:%s\n", mm2)
	}
}

二、go语言通道类型

通道(Channel)是Go语言中一种非常独特的数据结构。它可用于在不同Goroutine之间传递类型化的数据,并且是并发安全的。相比之下,整数类型,浮点数类型,复数类型,字符串类型,数组类型,切片类型,字典类型都不是并发安全的。

    Goroutine(也称为Go程序)可以被看做是承载可被并发执行的代码块的载体。

它们由Go语言的运行时系统调度,并依托操作系统线程(又称内核线程)来并发地执行其中的代码块。

    通道类型仅由两部分组成:关键字(chan) 可变数据类型 ,如:chan T    

    在这个类型字面量中,左边是代表通道类型的关键字chan,而右边则是一个可变的部分,即代表该通道类型允许传递的数据的类型(或称通道的元素类型)。

这两部分之间需要以空格分隔。

  

    与其它的数据类型不同,我们无法表示一个通道类型的值。因此,我们也无法用字面量来为通道类型的变量赋值。

通过调用内建函数make来表示一个通道类型的值。

make函数可接受两个参数。第一个参数是代表了将被初始化的值的类型的字面量(比如chan int),而第二个参数则是值的长度。

例如,初始化一个长度为5且元素类型为int的通道值,则需要这样写:make(chan int, 5)    

    make函数也可以被用来初始化切片类型或字典类型的值。

    确切地说,通道值的长度应该被称为其缓存的尺寸。换句话说,它代表着通道值中可以暂存的数据的个数。

注意,暂存在通道值中的数据是先进先出的,即:越早被放入(或称发送)到通道值的数据会越先被取出(或称接收)。

    声明一个通道类型的变量,并为其赋值:ch1 := make(chan string, 5)   

    我们就可以使用接收操作符<-向通道值发送数据,也可以使用它从通道值接收数据。

例如,向通道ch1发送字符串"value1",应该这样做:ch1 <- "value1"  

    从ch1那里接收字符串,则要这样:<- ch1    

    这时,我们可以直接把接收到的字符串赋给一个变量,如:value := <- ch1    

    与针对字典值的索引表达式一样,针对通道值的接收操作也可以有第二个结果值,如:value, ok := <- ch1     

    这样做的目的是为了消除与零值有关的歧义。

这里的变量ok的值同样是bool类型的。它代表了通道值的状态,true代表通道值有效,而false则代表通道值已无效(或称已关闭)。

更深层次的原因是,如果在接收操作进行之前或过程中通道值被关闭了,则接收操作会立即结束并返回一个该通道值的元素类型的零值。

按照上面的第一种写法,我们无从判断接收到零值的原因是什么。不过,有了第二个结果值之后,这种判断就好做了。

   

    调用内建函数close来关闭通道值,就像这样:close(ch1)   

    请注意,对通道值的重复关闭会引发运行时恐慌。这会使程序崩溃。所以一定要避免这种情况的发生。

另外,在通道值有效的前提下,针对它的发送操作会在通道值已满(其中缓存的数据的个数已等于它的长度)时被阻塞。

而向一个已被关闭的通道值发送数据会引发运行时恐慌。

另一方面,针对有效通道值的接收操作会在它已空(其中没有缓存任何数据)时被阻塞。

与切片和字典类型相同,通道类型属于引用类型。它的零值即为nil。

实例代码go_tongdao.go:

package main

import "fmt"

func main() {
	//初始化一个长度为1且元素类型为string的通道类型,并为其赋值
	ch2 := make(chan string, 1)
	// 下面就是传说中的通过启用一个Goroutine来并发的执行代码块的方法。
	// 关键字 go 后跟的就是需要被并发执行的代码块,它由一个匿名函数代表。
	//在花括号中的就是将要被并发执行的代码。
	go func() { //匿名函数
		ch2 <- ("已到达!") //发送数据
	}()
	//定义一个string类型的变量
	var value string = "数据"
	value = value + (<-ch2) //接收数据并进行拼接
	fmt.Println(value)
}



三、go语言通道分类

通道有带缓冲和非缓冲之分。缓冲通道中可以缓存N个数据。我们在初始化一个通道值的时候必须指定这个N。相对的,非缓冲通道不会缓存任何数据。发送方在向通道值发送数据的时候会立即被阻塞,直到有某一个接收方已从该通道值中接收了这条数据。

非缓冲的通道值的初始化方法如:make(chan int, 0)

缓冲的通道的初始化方法如:make(chan int, 5)

    注意,非缓冲通道给予make函数的第二个参数值是0。而缓冲通道给予make函数的第二个参数值是一个确定的数值。

  

    我们还可以以数据在通道中的传输方向为依据来划分通道。默认情况下,通道都是双向的,即双向通道。如果数据只能在通道中单向传输,那么该通道就被称作单向通道。

我们在初始化一个通道值的时候不能指定它为单向。但是,在编写类型声明的时候,我们却是可以这样做的。例如:

type Receiver <-chan int

    类型Receiver代表了一个只可从中接收数据的单向通道类型。这样的通道也被称为接收通道。

在关键字chan左边的接收操作符<-形象地表示出了数据的流向。

相对应的,如果我们想声明一个发送通道类型,那么应该这样:

type Sender chan<- int

    这次<-被放在了chan的右边,并且“箭头”直指“通道”。

我们可以把一个双向通道值赋予上述类型的变量,就像这样:

var myChannel = make(chan int, 3)    //带缓冲的通道

var sender Sender = myChannel    //发送通道

var receiver Receiver = myChannel    //接收通道

    但是,反之则是不行的。像下面这样的代码是通不过编译的:

var myChannel1 chan int = sender     

    单向通道的主要作用是约束程序对通道值的使用方式。

比如,我们调用一个函数时给予它一个发送通道作为参数,以此来约束它只能向该通道发送数据。

又比如,一个函数将一个接收通道作为结果返回,以此来约束调用该函数的代码只能从这个通道中接收数据。

实例代码go_tongdao1.go:

package main

import (
	"fmt"
	"time"
)

type Sender chan<- int   //发送通道
type Receiver <-chan int //接收通道

func main() {
	//定义一个非缓冲通道
	var myChannel = make(chan int, 0)
	var number int = 6
	//并发执行代码块
	go func() {
		//发送通道
		var sender Sender = myChannel
		sender <- number //向通道sender发送一个数据6
		fmt.Println("发送过去的数据是:", number)
	}()

	go func() {
		//接收通道
		var receiver Receiver = myChannel
		fmt.Println("接收到的数据是:", <-receiver)
	}()
	//让main函数执行结束的时间延迟1秒
	time.Sleep(time.Second)
}

上述实例的运行效果:



5人推荐
随时随地看视频
慕课网APP