Go并发编程实战笔记

一、Go语言的主要特征

1.开放源代码的通用计算机编程语言。

2.静态类型、编译形的语言,语法趋于校本化。

3.卓越的跨平台支持,无需移植代码。

4.全自动的垃圾回收机制,无需开发者干预。

5.原生的先进并发模型和机制。

6.拥有函数式编程范式的特性,函数为一等代码块。

7.无继承层次的轻量级面向对象编程范式。

8.Go语言的垃圾回收采用的是并发的标记清除算法(Concurrent Mark and Sweep,CMS)。虽然是并发的操作,时间比串型操作短很多,但是还是会在垃圾回收期间停止所有用户程序的操作。

 

二、包初始化

1.在Go语言中,可以有专门的函数负责代码包初始化。这个函数需要无参数声明和结果声明,且名称必须位init。

2.所有代码包初始化函数都会在main函数之前执行完成,而且只会执行一次。

3.当前代码包中的所有全局变量的初始化会在代码包初始化函数执行完成。

4.在同一个代码包中,可以存在多个代码包初始化函数。

5.Go语言编译器不能保证同一代码包中的多个代码包初始化函数的执行顺序,如果确实需要,可以考虑使用Channel进行控制。

 

三、语法与数据类型

基本词法:

1.Go语言源码文件必须是UTF-8编码格式的。

标识符:

2.当我们只想执行下某个代码包中的初始化函数,而不需要使用这个代码包中的任何程序实体的时候,可以这样编写导入语句:

import _ "runtime/cgo"

字面量:

3.用于表示复合数据类型的值的复合字面量。(Struct、Array、Slice、Map)

4.对复合字面量的每次求值都会导致一个新的值被创建。

类型:

5.Go语言中的类型可以分为静态类型和动态类型。

6.静态类型是指在变量声明中示出的那个类型,绝大多数类型的变量都只拥有静态类型。

7.动态类型代表了在运行时与该变量绑定在一起的值的实际类型,这个实际类型可以是实现了这个接口类型的任何类型。

8.唯独接口类型既有静态类型,又有动态类型。

9.接口类型的变量的动态类型可以在执行期间变化,因为所有实现了这个接口的类型的值都可以被赋给这个变量。但是,这个变量的静态类型永远只能是它声明时被指定的那个类型

10.每一个类型都有一个潜在类型。如果这个类型是一个预定义类型(基本类型),或者是一个由类型字面量构造的复合类型,那么它的潜在类型就是它本身。如果一个类型并不属于上述情况,那么这个类型的潜在类型就是在类型声明中的那个类型的潜在类型。

11.Go语言中rune可以看作是uint32类型的一个别名类型,其潜在类型就是uint32。

12.一个类型的潜在类型具有可传递性。

13.一个数组类型的潜在类型决定了在该类型的变量中可以存放哪一个类型的元素。

14.如果除数是零,但被除数是浮点数类型或复合类型,不一定会panic。

操作符:

15.当多个逻辑操作符和操作数进行串联时,Go语言总是从左到右依次对他们求值。

16.*被称为取值操作符,&被称为取址操作符。

17.++和--是语句而不是表达式。*p++等同于(*p)++。

表达式:

18.数组切片的长度范围就是int类型可以表示的非负的取值范围。

19.对于str[],如果str是字符串类型,则不能对str[0]进行赋值操作,因为字符串类型值是不能改变的。

20.当字典类型a的值为nil时,a[x]并不会发生任何错误,但是对a[x]进行赋值时却会引起恐慌。

21.在调用可变参数函数时,会创建一个类型相同的切片,用于存放实际参数。

22.我们可以直接把一个元素类型位T的切片类型赋值给...T类型的可变参数。格式:test([]int{1,2,3}...)

23.也可以将...T类型赋值给切片。

func test(i ...int){
   ints := []int{}
   ints = i
   fmt.Println(ints)
}

数据类型:

24.原生字符串是被两个反引号包括的` 。原生字符串中的回车符会被编译器移除。

25.数组的长度是数组类型的一部分,只要类型声明中的数组长度不同,即使两个数组的元素类型相同,他们也还是不同的类型。

26.数组赋值根据元素值递增规则推算。

i := [3]int{1:1,3}
fmt.Println(i)
[0 1 3]

27.指定的元素索引值不能与其他元素索引值重复,无论是索引值是显式对应还是隐式对应的。

s := [3]string{0:"a","b",1:"c"}

28.可以不显式的指定数组长度,用...填充,让编译器为我们计算所含元素个数确定长度。

s := [...]string{"a","b"}

29.数组索引不在范围内,在编译期间就会造成编译错误

30.切片是对数组的一种包装形式。

31.切片长度需要控制在intleasing所能表示的非负值范围之内。

32.一个切片值总会持有一个对某个数组值的引用。事实上,一个切片值一旦被初始化,就会与一个包含了其中元素值的数组值相关联。

33.多个切片值可能会共用同一个底层数组。

34.对作为底层数组的数组值中的元素值的改变,也会体现到引用该底层数组且包含该元素值的所有切片值上。

35.一个切片值的容量是从其中的指针指向的那个元素值,到底层数组的最后一个元素的计数值。

36.如果切片长度超过了原数组的长度,将有一个新的数组被创建并初始化。

37.为切片设置容量。1:保证只能修改底层数组指定索引内的值。2:避免长度超过底层数组而创建新数组,之前的数组修改入口丢失。

38.map的键必须是可比较的,也就是说,键的值必须可以作为比较操作符==和!=的操作数。

39.与指针类型和切片类型一样,字典类型是一个引用类型。与切片值相同,一个字典值总是会持有一个针对某个底层数据结构值的引用。

40.Go语言中只有“传值”而没有”传引用“。函数内部对参数值的修改是否会在函数外体现,只取决于被改变的值的类型是值类型还是引用

     类型。

41.一个值为nil的字典相当于长度为0的空字典,对其进行读操作不会引起任何错误,但是对它进行写操作会引起恐慌。

42.delete函数不存在返回值,也不会在运行时发生错误或恐慌。

43.值方法包含了与它关联的所有值方法,指针方法却包含了所有值方法和指针方法。

44.在指针方法中一定能够改变接受者的值,而在值方法中,对接收者的值的改变对于该方法之外一般是无效的。

45.接收者类型如果是引用类型的别名类型,那么在该类型的值方法中对该值的改变也是对外有效的。

46.切片类型和字典类型都是引用类型。除此之外,通道类型也是引用类型

47.Go语言对接口的实现是非入侵式的。要想实现一个接口,只需要实现其中的所有方法集合即可,而不需要在数据类型上添加任何标记。

48.一个接口类型只接收其他接口类型的嵌入,不能嵌入自身。这包括直接嵌入和间接嵌入。

49.一个接口类型可以被任意数量的数据类型实现,一个数据类型也可以同时实现多个接口类型。

50.任何较浅层次的嵌入类型的字段或方法都会隐藏较深层次的嵌入类型包含的同名字段或方法。

51.这种隐藏是可以交叉进行的,即字段可以隐藏方法,方法也可以隐藏字段。

52.如果在同一嵌入层次中的两个嵌入类型拥有同名字段或方法,那么涉及他们的表达式将造成编译错误。

53.不论从修改最小化还是维护性方面来看,扩展一个接口类型远远要比直接对这个接口类型修改好得多。

54.new函数用于为值分配内存。它并不会初始化分配到的内存,而只会清零它。

55.调用表达式new(T)被求值时,所做的是为T类型的新值分配并清零一块内存空间,然后将这块空间的地址返回,即*T。

56.new会立即得到一个可用的值的指针值,而不需要再做额外的初始化。

57.make函数只能被用于创建切片类型、字典类型和通道类型的值。并返回一个已经被初始化(非零值)的对应类型的值。

58.这么做的原因是因为它们都是引用类型,在它们的每一个值的内部都保持着一个对某个底层数据结构值的引用。如果不对它们的值

     进行初始化,那么其中的这种引用关系是不会被建立起来的,同时相关的内部值也会不正确。在这种情况下,该类型的值就不能被

     使用,因为它们是不完整的,还处于未就绪的状态。

59.切片类型、字典类型和通道类型的零值都是nil,而不是那个未被初始化的值。当new这三种类型时,得到的是指向空值nil的指针值。

60.内建函数make与new接收的函数也有所不同。对于切片来说,我们可以把新值的长度和容量传递给make函数,如果不指定容量,将与

     长度一致。

61.make函数只能被应用在引用类型的值的创建上。并且,它的结果是第一个参数所代表的类型的值,而不是指向这个值的指针值。

数据的使用:

62.字面量可以用于初始化几乎所有的Go语言数据类型的值,除了接口和通道类型。接口类型没有值,通道类型只能使用make函数创建。

63.赋值语句的执行分两个阶段。第一个阶段,表达式按顺序被求值。(赋值操作符左边的索引表达式-->取址表达式的操作数-->

     赋值操作符右边表达式);第二个阶段,赋值会以从左到右的顺序执行。

64.平行赋值永远是从左向右执行的,即使靠右的赋值发生了恐慌,它左边的赋值依然会生效。

65.操作数为无类型常量的移位操作的结果总会是一个无类型的整数常量。

66.在同一条常量声明语句中,iota代表的整数常量是否递增取决于是否又有一个常量声明包含了它,而不是它是否又在常量声明中出现

     了一次。

const (
   e,f = iota,iota
   g,h
   i,j
)

func main() {
   fmt.Println(e,f,g,h,i,j)
}

输出0 0 1 1 2 2

67.我们可以利用_跳过iota表示的递增序列中的某个值或某些值。

68.对于两个结构体来说,如果它们之中的字段声明的数量是相同的,并且在对应位置上的字段具有相同的字段名称和恒等的数据类型,

     那么这两个结构体数据类型就是恒等的。如果一个结构体含匿名字段,那么另一个结构体的对应字段也必须匿名;结构体声明中的

     字段标签(如`json:`)也应该作为恒等判断的依据。

69.对于指针,如果两个指针的基本类型恒等(也就是它们指向的那个类型),那么它们就是恒等的。

70.函数类型的恒等判断并不会以参数和结果的名称为依据,而只关心它们的数量,类型和位置。

71.参数中切片类型和可变长度不恒等。

72.对于两个接口类型,如果它们拥有相同的方法集合,那么它们就是恒等的。方法声明的顺序是无关紧要的。

73.如果两个数据类型在不同的代码包中,即使满足上述相关规则也不是恒等的。

74.分别指向两个不同的、大小为零的变量的指针有可能是相等的也可能是不相等的。

75.大小为零的变量是指无任何字段的结构体值或无任何元素的数组值。大小为零的变量可能会有相同的内存地址,因为它们在本质上

     很可能是没有区别的。

76.通道类型具有可比性。如果两个通道类型值的元素类型和缓冲区大小都一致,那么就可以被判定为相等。另外,如果两个通道类型的

     变量的值都是nil,那么它们也是相等的。

func main() {
   var c1 chan int
   var c2 chan int
   c3 := new(chan int)
   c4 := make(chan int)
   c5 := make(chan int,1)
   c6 := make(chan int,1)
   c7 := make(chan int)

   fmt.Println(c1 == c2)
   fmt.Println(c1 == *c3)
   fmt.Println(c1 == c4)
   fmt.Println(c5 == c6)
   fmt.Println(c5 == c7)
   fmt.Println(c4 == c7)
}

输出:

true
true
false
false
false
false

77.接口类型值具有可比性。如果两个接口类型值拥有相等的动态类型和相同的动态值,那么就可以判定它们是相等的。

     如果我们有一个接口类型的变量,那么在这个变量中就只能存储实现了该接口类型的类型值。我们把存储在该变量中的那个值的类型

     叫做该变量的动态类型,而把这个值叫做该变量的动态值。另外,如果两个接口类型的变量的值都是空值,那么它们也是相等的。

78.非接口类型X的值x可以与接口类型T的值t判断相等,当且仅当接口类型T具有可比性且类型X是接口类型T的实现类型。如果值t的动态

     类型与类型X恒等并且值t的动态值与值x相等,那么就可以说值t和值x就是相等的。除上述情况之外,不同数据类型的值相比较,会造

     成编译错误。

79.如果一个结构体类型中的所有字段都具有可比性,那么这个结构体类型就具有可比性。如果两个结构体对应字段相等,那么两个结构体

     就是相等的。切片类型的值不具有可比性。

80.数组类型具有可比性,当且仅当元素类型的值具有可比性。

81.一个例外情况是,在判断两个具有相同接口类型的值是否相等时,如果它们的动态类型不具有可比性就会引发恐慌。

82.切片类型、字典类型和函数类型的值是不具有可比性的。然而,作为特例,这些值可以与空值nil进行判等。

83.对于非常量x,如果它能够被转换为类型T的值,那么它肯定符合下列情况的一种。

     (1).值x可以被赋给类型T的变量。

     (2).值x的类型和类型T的潜在类型是相等的。

     (3).值x的类型和类型T都是未命名的指针类型,并且它们的基本类型(指向的那个值的类型)的潜在类型是相等的。

     (4).值x的类型和类型T都是整数类型或浮点数类型。

     (5).值x的类型和类型T都是复数类型。

     (6).值x是一个整数类型值或是一个元素类型为byte或rune的切片类型值,且T是一个string类型。

     (7).值x是一个string类型值,且T是一个元素类型为byte或rune的切片类型。

84.byte类型值和rune类型值都属于整数值的一种。

85.内建函数close只接收通道类型的值作为参数。

86.调用这个close函数之后,会使作为参数的通道无法再接受任何元素值。若试图关闭一个仅能接收元素值的通道,则会造成编译错误。

87.在通道关闭之后,再向它发送元素值或再次关闭它的时候,将会引发运行时恐慌。关闭为nil的通道也会引发恐慌。

四、流程控制方法

88.if语句和switch语句都可以接受一个可选的初始化子语句。

89.switch分为表达式switch和类型switch。

90.如果在switch语句中没有显式的switch表达式,那么true将作为switch表达式,所有的case表达式的结果都应是布尔类型。

func main() {
   i := 2

   switch {
   
      case i < 1:
         i++
      case i > 1:
         i--
      default:
         fmt.Println("default")
   }
}

91.fallthrough语句只能够作为case语句中的最后一条语句,更重要的是,该关键字不能出现在最后一个case中。

92.fallthrough不能出现在类型switch中。

func main() {
   s := "中国"
   var inter interface{}
   inter = s
   
   switch i := inter.(type) {
   case int:
      fmt.Println("i==int,%T\n",i)
   case string:
      fmt.Printf("i==string,%T\n",i)
   default:
      fmt.Println("default")
   }
}

输出:

i==string,string

93.一般情况下,range表达式只会在迭代开始前被求值一次。

94.对于一个字符串值来说,在一个连续的迭代之上产出的索引值(第一迭代值)既是某一Unicode代码点(与rune类型值一一对应)的

     UTF-8编码值中的第一个字节在与其所属的[]byte类型上的索引值。

s := "Golang爱好者"
for index,str := range s {
   fmt.Println()
   fmt.Println("index-->" + strconv.Itoa(index) + "str:" + string(str))
}

输出:

index-->0str:G

index-->1str:o

index-->2str:l

index-->3str:a

index-->4str:n

index-->5str:g

index-->6str:爱

index-->9str:好

index-->12str:者

95.对于一个字典值来说,它的迭代顺序是不固定的。如果字典中的键值对在还没有被迭代到的时候就被删除了,那么相应的迭代值就不会

     产出。另一方面,如果我们在字典值被迭代的过程中添加新的键值对,那么相应的迭代值是否会产出是不确定的。

func main() {
	testMap := map[int]string{
		1:"a",
		2:"b",
		3:"c",
	}
	go func() {
		delete(testMap,1)
		testMap[5] = "e"
		delete(testMap,2)
		testMap[4] = "d"
		delete(testMap,3)
		testMap[6] = "f"
	}()
	for index,value := range testMap {
		fmt.Println("index:"+strconv.Itoa(index)+"   value:"+value)
	}
}

96.如果range的表达式求值结果是一个通道类型,那么只会产出一个迭代值。

97.使用go语言for语句写出一个反转切片中左右数据值的方法。

func main() {
	list := []int{0,1,2,3,4,5,6,7,8,9}

	for i,j := 0,len(list);i < j/2; i++ {
		list[i],list[j - i - 1] = list[j - i - 1],list[i]
	}
	fmt.Println(list)
}

98.for子句的前置初始化子句和后置子句只能是单一语句而不能是多个语句,但是能够使用平行赋值。

99.当外围函数的函数体中的return语句被执行的时候,只有在该函数中的所有defer语句都被执行完毕之后才会正真的返回。

100.当在外围函数中有运行时恐慌发生时,只有该函数中所有的defer语句都执行完毕之后,运行时恐慌才会真正的扩散至该函数调用方。

101.在recode函数执行之触begin函数就已经被调用了,而end函数的调用却是在recode函数结束的前一刻。

func begin(funcName string) string {
	fmt.Println("Begin function recode")
	return funcName
}

func end(funcName string) string {
	fmt.Println("End function recode")
	return funcName
}

func recode()  {
	defer end(begin("recode"))
	fmt.Println("In function recode")
}

func main() {
	recode()
}


Begin function recode
In function recode
End function recode

102.这样做可以避免参数值在延迟函数被真正调用之前再次发生改变而给该函数的执行造成影响之外,还是出于同一条defer语句可能会

       被多次执行考虑。

func main() {
	for i := 0; i < 5 ; i++ {
		defer fmt.Println(i)
	}
}



4
3
2
1
0

103.Go语言会把代入参数值之后的调用表达式另行存储。

104.每当Go语言把已代入参数值的延迟函数调用表达式另行存储之后,还会把它追加到一个专门为当前外围函数存储延迟函数调用表达式

       的列表当中。这个列表总是LIFO(后进先出)的。

105.在defer语句被执行的时候传递给延迟函数的参数都会被求值,但是延迟函数调用表达式并不会在那时求值。

func main() {
	for i := 0; i < 5 ; i++ {
		defer func() {
			fmt.Println(i)
		}()
	}
}


5
5
5
5
5

106.延迟函数中的代码是可以对命名结果值进行访问和修改的,其次,虽然延迟函数中可以包含结果声明,但是其返回结果会在它被执行

       完毕时丢弃。

107.除了errors.New函数外,另一个可以生成error值的方法是调用fmt包的Errorf函数。其内部的创建和初始化也是通过errors.New函数

       来完成的。

// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...interface{}) string {
	p := newPrinter()
	p.doPrintf(format, a)
	s := string(p.buf)
	p.free()
	return s
}

// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a ...interface{}) error {
	return errors.New(Sprintf(format, a...))
}

108.如果运行时恐慌重新被引发,在调用栈信息中,不但会包含该恐慌首次引发时的调用轨迹和位置,还会包含重新引发该恐慌的位置。

109.在java语言中java.util.HashSet类就是用java.util.HashMap类作为底层支持的。

110.Go语言怎样生成interface{}类型的hash值?

       当我们把一个interface{}类型值作为键添加到一个字典值的时候,Go语言会先获取这个值的实际类型(动态类型),然后再使用对应的

       hash函数对该值进行hash计算。在之后的键查找过程中也会存在这样的hash计算。

111.sort.Sort函数使用的排序算法是一种由三项切分的快速排序、堆排序、插入排序组成的混合算法。不保证稳定性。

112.虽然快速排序是最快的通用排序算法,但在元素值很少的情况下,它比插入排序要慢一些。

       而堆排序的空间复杂度是常数级别的,且它的时间复杂度在大多数情况下只略逊于其它两种排序算法。

       所以在快速排序中的递归达到一定深度时,切换至堆排序来节省空间是值得的。

五、程序测试和文档

113.测试文件以xxx_test.go命名,函数以TestXxx命名,入参可使用 t *testing.T 类型的参数。

114.测试命令 go test:

       -run标记:-run标记值是一个正则表达式,名称与此表达式匹配的函数才会被执行。

       -timeout:为程序运行设置时间,如果超时将会造成恐慌。

       -short:告诉程序尽量缩短运行时间,具体怎样缩短,由测试函数自己实现。(testing.Short函数会返回布尔型的值。)

       -parallel:设置允许并发执行的功能测试函数的最大数量。先决条件:在功能测试函数开始处加入代码t.Parallel()。

                        -parallel标记是通过代码库runtime.GOMAXPROCS()函数设置的。   

115.基准测试的函数以BenchmarkXxx命名,入参可使用 b *testing.B 类型的参数。

116.与定时器相关的函数有三个,b.StartTimer、b.StopTimer、b.ResetTimer。

       b.StartTimer方法意味着开始对当前测试函数的执行进行计时,它总会在开始执行测试函数时自动执行。因此,这个方法暴露

       的意义在于:计时器在被停止之后重新启动。相应的,b.StopTimer方法可使计时器停止。

117.b.ResetTimer会重置当前的计时器,也就是说,把该函数的执行时间重置为0.

118.b.setBytes方法被用于记录在单次操作中被处理的字节数量。

六、并发编程综述

119.并发程序是指可以被同时发起执行的程序,而并行程序则是被设计成可以在并行的硬件上执行的并发程序。

       换句话说,并发程序代表了所有可以实现真正的或者可能的并发行为的程序。并行程序时并发程序的一种。

120.并发程序内部的交互方式:同步(异步)和传递数据。

121.多进程通讯(IPC)从处理方式上看分为三类:基于通讯的IPC方法、基于信号的IPC方法、基于同步的IPC方法

       基于通讯的IPC:

               以数据传送为手段的IPC方法:

                       管道:传送字节流。

                       消息队列:传送结构化的消息对象。

               以共享内存为手段的IPC方法:

                       以共享内存区为代表,是最快的一种IPC方法。

       基于信号的IPC方法:

               操作系统的信号机制,唯一一种异步的IPC方法。

      基于同步的IPC方法:

               最重要的就是信号灯。

122.我们通常把一个程序的执行成为一个进程。反过来讲,进程被用于描述程序的执行过程。因此,程序与进程是一对相依赖的概念。

       它们分别描述了一个程序的静态形式和动态特征。进程还是操作系统进行资源分配的基本单位。

123.在Unix/Linux操作系统中,每一个进程都有父进程。所有的进程共同组成了一个树状结构。内核启动进程作为进程树的根并负责系统

       的初始化工作。它是所有进程的祖先,它的父进程是它本身。如果某一个进程先于它的子进程结束,那么这些子进程将会被内核启动

       进程”收养“,成为它的直接子进程。

124.为了管理进程,内核会将每个进程的信息记录在进程描述符中。进程ID(常被称为PID)是进程在操作系统中的唯一标示。

       进程ID为1的进程就是内核启动进程,新被创建的进程ID是上一个递增的结果。进程ID可以被重复使用。

       当进程ID达到最大限值时,内核会从头开始寻找闲置的ID。

       进程描述符中还会包含当前进程的父ID(PPID)。

       Go语言查看当前进程PID和PPID的API:

               pid := os.Getpid()

               ppid := os.Getppid()

       PID并不传达与进程有关的任何信息,它只是用来唯一标识进程的数字而已。

125.在Linux操作系统中,进程可能的状态一共有六个(暂停状态和调试状态十分相似,但是也可以看成两种状态,所以可以说7种状态)。

       可运行状态、可中断的睡眠状态、不可中断的睡眠状态、暂停状态或跟踪状态、僵尸状态和退出状态。

      

126.Linux操作系统将物理内存分为内核空间和用户空间。用户进程都存在于用户空间,用户空间不能与硬件进行交互。

       内核可以与硬件交互,但是内核生存在内核空间。用户无法直接访问内核空间。

       32位的计算机可以有效标识2的32次方个内存单元,64位则可以标识2的64次方个。

127.这里所说的地址并非物理内存中的真实地址,它们被称为虚拟地址。

       虚拟内存的最大容量与实际可用的物理内存的大小是无关的。

       内核和cpu会负责维护虚拟内存与物理内存之间的映射关系。

       内核会为每个用户进程分配的是虚拟内存而不是物理内存。

       进程之间的虚拟内存几乎是彼此独立互不干扰的。

128.内核会暴露出一些接口,这些接口是用户进程使用内核进程(包括操纵计算机硬件)的唯一手段。

       当用户进程发出一个系统调用的时候,内核会把CPU从用户态切换到内核态。

       CPU在内核状态下才有权访问内核空间。

129.我们把执行过程中不能被中断的操作称为原子操作,而把只能被串行化的访问或执行的某个资源或某段代码成为临界区。

       所有的系统调用都属于原子操作。

       原子操作不能被中断,临界区只要保证一个访问者在临界区的时候其它访问者不会被放进来就可以了。它们的强度是不同的。

       保证只有一个进程或线程在临界区之内的这种做法被称为--互斥,实现互斥的方法必须确保“排他原则”。

130.管道是一种半双工(单向的)的通讯方式,它只能用于父进程与子进程以及同祖先的子进程之间的通讯。

       我们在使用Shell命令的时候经常用到管道,Go语言是支持管道的。

cmd0 := exec.Command("echo","-n","Hello world")

       使用exec.Command类型的Start方法可以启动一个命令,

if err := cmd0.Start(); err != nil{
		println(err)
	}

       但是为了创建一个能够获取此命令的输出管道,我们需要在上面这条if语句之前加入下面的代码:

stdout0,err := cmd0.StdoutPipe()
	if err != nil {
		return
	}

       变量cmd0的StdoutPipe方法会返回一个输出管道,它的类型是io.ReadCloser。之后我们可以调用stdout0的read方法获取命令输出。

outputBuf0 := bufio.NewReader(stdout0)
	output0,_,err := outputBuf0.ReadLine()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(output0))

       bufio.NewReader方法会返回一个bufio.Reader类型的值,被称为缓冲读取器。默认情况下,该读取器会携带一个长度为4096的缓冲

               区,缓冲区代表了我们一次可读取的最大数量。

131.管道可以把一个命令的输出做为另一个命令的输入。

cmd1 := exec.Command("ps","aux")
	cmd2 := exec.Command("grep","apipe")
	stdout1,err := cmd1.StdoutPipe()
	if err != nil {
		return
	}
	if err := cmd1.Start(); err != nil{
		fmt.Println(err)
		return
	}

	outputBuf1 := bufio.NewReader(stdout1)
	output1,_,err := outputBuf1.ReadLine()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(output1))

	stdin2,err := cmd2.StdinPipe()
	if err != nil {
		fmt.Println(err)
		return
	}

	outputBuf1.WriteTo(stdin2)

       我们通过StdinPipe方法在cmd2上创建输入管道,并把与cmd1连接的输出管道的数据全部写入到输入管道中。

       我们还需要启动cmd2并关闭与它连接的管道,以完成数据传递。

var output2 bytes.Buffer
	cmd2.Stdout = &output2
	if err := cmd2.Start(); err != nil{
		fmt.Println(err)
		return
	}
	err = stdin2.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(output2)

       为了获取到cmd2的所有输出内容,我们需要等到它运行结束,再去查看缓冲区内容。

       方法Wait会一直阻塞到其所属命令完全运行结束为止。

if err := cmd2.Wait(); err != nil {
		fmt.Println(err)
		return
	}

132.上面所说的管道都属于匿名管道,与此相对的是命名管道。与匿名管道不同的是,任何进程都可以通过命名管道交换数据。实际上,

       命名管道以文件的形式存在于文件系统中。使用它的方法与使用文件很类似,Linux操作系统支持使用Shell命令创建和使用管道。

       先试用命令mkfifo在当前目录创建了一个命名管道myfifo1,然后使用这个命名管道和tee命令把src.log文件中的内容写到dst.log中。

133.我们可以使用命名管道运输数据,实现数据的过滤和转换,以及管道的多路复用等功能。

       命名管道默认是阻塞式的,只有在对这个命名管道读操作和写操作都准备就绪之后,数据才会开始流转。

       相对于匿名管道,命名管道最大的优势就是通讯双方可以毫不相关。并且我们可以使用它建立非线性的连接以实现数据的多路复用。

       但命名管道仍然是单向的,由于我们可以在命名管道之上实现多路复用,所以有时候也需要考虑多个进程同时向命名管道写数据的

       情况下的原子操作性问题。

134.cmd对象的三个方法StdoutPipe、StdinPipe和StderrPipe分别代表标准输出、标准输入和错误输入。

       https://www.jianshu.com/p/d5ea4a8acfb9

135.Go语言提供了创建命名管道的方法。

reader,writer,err := os.Pipe()

       管道都是单向的,我们不能反过来使用reader和writer。另外,无论在哪一方调用Close方法,都不会影响另一端的读取或写入操作。

       实际上,exec.Cmd类型之上调用StdinPipe和StdoutPipe方法后得到的输入管道或输出管道都是通过os.Pipe函数生成的,只不过,在

       这两个方法内部又对生成的管道做了少许处理。输入管道的输出端会在所属命令启动后就立即被关闭,而输入端则会在所属命令运行

       结束后关闭。而输出管道两端的自动关闭时机与前面刚好相反。

       有些命令会等到输入管道被关闭之后才会结束运行,所以,我们就需要在数据被读取之后尽早的手动关闭输入管道。

136.由于输出管道实际上也是有os.Pipe函数生成的,所以我们在使用某个exec.Cmd类型值上的输出管道的时候需要有所注意。

       我们不能在调读完输出管道中的全部数据之前调用该值的Wait方法。

       只要我们建立了对应的输出管道,就不能使用Run方法来启动该命令,而应该使用Start方法。

137.通过os.Pipe函数生成的管道在底层是由操作系统管道支持的,命名管道可以被多路复用,操作系统提供的管道不提供原子性操作。

       为此Go语言标准库代码包io中提供了一个被存于内存中、有原子性操作保证的管道(内存管道)。

pipeReader,pipeWriter := io.Pipe()

       为了避免使用者对管道的反向使用,在*PipeReader类型的值上我们只能使用Read方法读取数据,*PipeWriter同理。

       我们在使用Close方法关闭管道的某一端之后,另一端在读取或写入数据时会得到一个可预定义的error值。

138.内存管道的内部是充分使用sync代码包中提供的API来从根本上保证操作的原子性的,我们可以在它之上放心的写入和读取数据。

       由于这种管道不是基于文件系统,并没有作为中介的缓冲区,所以它传递数据只复制一次。

139.操作系统信号本质是用软件来模拟硬件的中断机制。

140.Linux操作系统支持的信号又62种。编号1-31的信号属于标准信号(不可靠信号),编号34-64的信号属于实时信号(可靠信号)。

       对于同一个进程来说,每种标准信号只会被记录并处理一次。如果发送给某一进程的标准信号的种类有多个,那么它们的处理顺序是

       完全不确定的。而实时信号解决了标准信号的这两个问题。

141.信号的来源有键盘输入、硬件故障,系统函数调用和软件中的非法运算。进程响应信号的方式有3种:忽略、捕捉和执行默认操作。

       Linux系统对每一个标准信号都有默认的操作方式。针对不同种类的标准信号,其默认的操作方式一定会是以下操作中的一个:

       终止进程、忽略该信号、终止进程并保存内存信息、停止进程、若进程已停止就恢复。

142.Go命令会对其中的一些以键盘输入为来源的标准信号做出响应,这是由于go命令使用了在标准代码包os/signal中的被用于信号处理的

       API。更具体的讲,go命令指定了需要被处理的信号并使用了通道类型的变量来监听信号的到来。

type Signal interface {
	String() string
	Signal() // to distinguish from other Stringers
}

       所有此接口类型的实现类型值都应该代表一个操作系统信号,理所当然,每一个操作系统信号都是需要操作系统支持的。

       在Go语言标准库代码包syscall中,已经为不同操作系统的所支持的标准信号都生成了一个相应同名常量(即信号常量)。

       这些信号常量的类型都是syscall.Signal,syscall.Signal是os.Signal接口类型的一个实现类型,同时也是int类型的别名类型。这就

       意味着每个信号常量都隐含着一个整数值,而信号常量的整数值与它代表的信号在所属操作系统中的编号是一致的。

143.代码包os/signal中的Notify用来把操作系统发给当前进程的指定信号通知给该函数的调用方。

func Notify(c chan<- os.Signal, sig ...os.Signal)

       signal.Notify函数会把当前进程接收到指定信号放入参数c代表的通道类型值中。这样,调用方代码就可以从这个signal接收通道中按

       顺序的获取到操作系统发来的信号并进行相应的处理了。

       函数signal.Notify的第二个参数是一个可变长参数。参数sig代表的参数值是我们希望自行处理的所有信号。

sigRrcv := make(chan<- os.Signal,1)
sigs := []os.Signal{syscall.SIGINT,syscall.SIGQUIT}
signal.Notify(sigRrcv,sigs...)
for sig := range sigs {
    fmt.Println(sig)
}

       在sigRrcv代表的通道类型值被关闭后,for会立即退出执行。

       signal处理程序在向signal接收通道发送值的时候,并不会因为通道已满而产生阻塞。所以signal.Notify函数的调用方必须保证接收

       通道有足够的空间缓存并传递接收到的信号。我们可以创建一个足够长的signal通道,但是更好的解决办法是,只创建一个长度为1

       的signal通道,并且时刻准备从该通道中接收信号。

144.如果当前进程接收到了一个我们不想自行处理的信号,则执行操作系统默认操作。所以,如果我们指定了想要自行处理的信号,但又

       没有在接收到信号时执行必要的处理动作,就相当于使当前进程忽略这些信号。

145.在类Unix操作系统下,有两种信号不能被自行处理也不会被忽略。它们是:SIGKILL和SIGSTOP。

146.对于其它信号,我们除了能够自行处理他们之外,还可以在之后的任意时刻恢复针对它们的系统默认操作。这需要使用到os/signal

       包中的Stop函数。

func Stop(c chan<- os.Signal)

       函数signal.Stop会取消掉在之前调用signal.Notify函数的时候告知signal处理程序需要自行处理若干信号的行为。只有我们把当初出

       传给signal.Notify函数的那个signal接收通道作为调用signal.Stop函数时的参数值,才能取消掉之前的行为,否则将不会起任何作用。

       这里存在一个副作用,之前用于从signal通道接收信号值的for语句将一直被阻塞。为了消除这种副作用,我们可以调用signal.Stop

       函数之后使用内建函数close关闭该signal接收通道。

signal.Stop(sigRrcv)
close(sigRrcv)

       signal接收通道被关闭后,被用于接收信号的for语句就会退出执行。

147.很多时候,我们只想取消部分信号的自行处理行为,我们只需再次调用signal.Notify函数,并重新设定与其参数sig绑定的值,只要

       作为第一个参数的signal接收通道相同就可以。

       如果signal接收通道的值不同,那么signal处理程序会视为这两次调用毫不相干,它会分别看待这两次调用时所设定的信号的集合。

148.在signal处理程序内部,存在一个包级私有的字典(信号集合字典)。该信号集合字典被用于存放以signal接收通道为键,以信号集合的

       变体为元素的键值对。当我们调用signal.Notify函数的时候,signal处理程序就会在信号集合字典中查找对应的键值对,如果不存在就

       添加,否则更新。当我们调用signal.Stop函数的时候,signal处理程序会删除相应的键值对。

       当接收到一个发送给当前进程且已被标识为应用程序想自行处理的操作系统信号时,signal处理程序会对它进行封装,然后遍历该map

       查看它们的元素是否包含该信号,如果包含会立即发送给作为键的signal接收通道。

149.我们可以编写一个向进程发送信号的程序,主要依靠结构体类型os.Process和相关的函数和方法。

       我们可以使用os.StartProcess函数启动一个进程,或者使用os.FindProcess函数查找一个进程。这两个函数都会返回一个

       os.*Processde的值(进程值)和一个error值。然后我们可以调用进程值的Signal方法来向该进程发送信号。

150.利用signal可以在进程被终止前释放所持的系统资源和持久化一些重要数据,还可以在当前进程中有效的控制其它相关进程的状态。

151.信号和管道都被称为基础的IPC方法。但是管道并不提供原子性操作,Go语言的标准库API也没有附加这种保证。

152.Socket,常被译为套接字。也是一种IPC方法,它是通过网络连接来使两个或更多的进程建立通讯并相互传递数据的。

 

 

 

 

 

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值