#2020学习打卡##Go语言高级编程# 什么是第一类对象?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 437 次浏览 • 2020-06-09 17:23 • 来自相关话题

#2020学习打卡##Go语言高级编程# Go语言中的函数闭包

zkbhj 发表了文章 • 0 个评论 • 307 次浏览 • 2020-06-09 16:54 • 来自相关话题

今天的学习内容是Go函数,这里面会涉及一个概念——闭包!复习一下~
 
闭包的概念

是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。

各种专业文献的闭包定义都非常抽象,我的理解是: 闭包就是能够读取其他函数内部变量的函数。

在javascript语言或者go中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。

所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

闭包的价值 

闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。


闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在函数调用后被自动清除。

Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。 
总结

闭包并不是一门编程语言不可缺少的功能,但闭包的表现形式一般是以匿名函数的方式出现,就象上面说到的,能够动态灵活的创建以及传递,体现出函数式编程的特点。所以在一些场合,我们就多了一种编码方式的选择,适当的使用闭包可以使得我们的代码简洁高效。

使用闭包的注意点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
 下面用一个例子体现闭包
package main

import "fmt"
/*
李逵和武松的Study方法的逻辑是几乎一模一样的
然而为了分别保存两人的学习进度,需要开辟两个全局变量,函数内部的需要使用两条分支结构才能完成业务逻辑
如果是108将都来学习。。。
此时代码的可复用性很差
*/
var progress int
func Study(name string, hours int) ( int) {
fmt.Printf("%s学习了%d小时",name,hours)
progress += hours
return hours
}
func main081() {
progress := Study("黑旋风",5)
fmt.Printf("李逵的学习进度%d/10000",progress)
}

/*
使用闭包函数优化Study
每个人有不同的学习进度,将这个进度保存在【各自的闭包】中
*/
/*
闭包函数:返回函数的函数
闭包函数的好处:使用同一份内层函数的代码,创建出任意多个不同的函数对象,这些对象各自的状态都被保存在函数闭包(外层函数)中,各行其道,互不干扰
*/

func GetStudy(name string) func(int) int{
var progress int
study := func(hours int) int {
fmt.Printf("%s学习了%d小时\n", name ,hours)
progress += hours
return progress
}
return study
}

func main() {
studyFunc := GetStudy("李逵")
studyFunc(3)
studyFunc(5)
likuiProgress := studyFunc(2)
fmt.Printf("李逵的学习进度%d/10000\n",likuiProgress)
studyFunc1 := GetStudy("宋江")
studyFunc1(9)
studyFunc1(8)
songjiangProgress1 := studyFunc1(5)
fmt.Printf("宋江的学习进度%d/10000\n",songjiangProgress1)
}李逵学习了3小时
李逵学习了5小时
李逵学习了2小时
李逵的学习进度10/10000
宋江学习了9小时
宋江学习了8小时
宋江学习了5小时
宋江的学习进度22/10000
 
 
 
参考文档:
https://www.cnblogs.com/cxying93/p/6103375.html
https://www.cnblogs.com/hzhuxin/p/9199332.html
https://www.cnblogs.com/yunweiqiang/p/11796135.html
  查看全部
今天的学习内容是Go函数,这里面会涉及一个概念——闭包!复习一下~
 
闭包的概念


是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。


各种专业文献的闭包定义都非常抽象,我的理解是: 闭包就是能够读取其他函数内部变量的函数

在javascript语言或者go中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。

所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁

闭包的价值 


闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回



闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在函数调用后被自动清除。

Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。 
总结

闭包并不是一门编程语言不可缺少的功能,但闭包的表现形式一般是以匿名函数的方式出现,就象上面说到的,能够动态灵活的创建以及传递,体现出函数式编程的特点。所以在一些场合,我们就多了一种编码方式的选择,适当的使用闭包可以使得我们的代码简洁高效

使用闭包的注意点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
 下面用一个例子体现闭包
package main

import "fmt"
/*
李逵和武松的Study方法的逻辑是几乎一模一样的
然而为了分别保存两人的学习进度,需要开辟两个全局变量,函数内部的需要使用两条分支结构才能完成业务逻辑
如果是108将都来学习。。。
此时代码的可复用性很差
*/
var progress int
func Study(name string, hours int) ( int) {
fmt.Printf("%s学习了%d小时",name,hours)
progress += hours
return hours
}
func main081() {
progress := Study("黑旋风",5)
fmt.Printf("李逵的学习进度%d/10000",progress)
}

/*
使用闭包函数优化Study
每个人有不同的学习进度,将这个进度保存在【各自的闭包】中
*/
/*
闭包函数:返回函数的函数
闭包函数的好处:使用同一份内层函数的代码,创建出任意多个不同的函数对象,这些对象各自的状态都被保存在函数闭包(外层函数)中,各行其道,互不干扰
*/

func GetStudy(name string) func(int) int{
var progress int
study := func(hours int) int {
fmt.Printf("%s学习了%d小时\n", name ,hours)
progress += hours
return progress
}
return study
}

func main() {
studyFunc := GetStudy("李逵")
studyFunc(3)
studyFunc(5)
likuiProgress := studyFunc(2)
fmt.Printf("李逵的学习进度%d/10000\n",likuiProgress)
studyFunc1 := GetStudy("宋江")
studyFunc1(9)
studyFunc1(8)
songjiangProgress1 := studyFunc1(5)
fmt.Printf("宋江的学习进度%d/10000\n",songjiangProgress1)
}
李逵学习了3小时
李逵学习了5小时
李逵学习了2小时
李逵的学习进度10/10000
宋江学习了9小时
宋江学习了8小时
宋江学习了5小时
宋江的学习进度22/10000

 
 
 
参考文档:
https://www.cnblogs.com/cxying93/p/6103375.html
https://www.cnblogs.com/hzhuxin/p/9199332.html
https://www.cnblogs.com/yunweiqiang/p/11796135.html
 

#2020学习打卡##Go语言高级编程# Go语言中的Nil到底是什么?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 391 次浏览 • 2020-06-08 17:26 • 来自相关话题

#2020学习打卡##Go语言高级编程# 什么是鸭子对象模型?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 408 次浏览 • 2020-06-08 16:01 • 来自相关话题

#2020学习打卡##Go语言高级编程# 详细理解数组array、字符串string和切片slice的异同

zkbhj 发表了文章 • 0 个评论 • 212 次浏览 • 2020-06-08 12:37 • 来自相关话题

Go语言中数组、字符串和切片三者是密切相关的数据结构。这3种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。

Go语言的数据是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。

Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导师底层数据的复制。

切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数的传参时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。
 
数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值)。和数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片的工作原理还是要先理解数组。

定义方式
var a [3]int // 定义长度为3的int型数组, 元素全部为0
var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2} // 定义长度为3的int型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。

第二种方式定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。

第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。这种初始化方式和 map[int]Type 类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用0值初始化。

第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三第四个元素零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。
 数组与指针

Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如C语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。
var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针
fmt.Println(a[0], a[1]) // 打印数组的前2个元素
fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。内置函数 len 可以用于计算数组的长度, cap 函数可以用于计算数组的容量。不过对于数组类型来说, len 和 cap 函数返回的结果始终是一样的,都是对应数组类型的长度。

遍历for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d\n", i, c[i])
}用 for range 方式迭代的性能可能会更好一些,因为这种迭代可以保证不会出现数组越界的情形,每轮迭代对数组元素的访问时可以省去对下标越界的判断。

字符串

一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。

Go语言字符串的底层结构在 reflect.StringHeader 中定义:
type StringHeader struct {
Data uintptr
Len int
}字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。

字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程。

字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问的同一块内存数据(因为字符串是只读的,相同的字符串面值常量通常是对应同一个字符串常量):
s := "hello, world"
hello := s[:5]
world := s[7:]
s1 := "hello, world"[:5]
s2 := "hello, world"[7:]切片

切片(slice)是一种简化版的动态数组。切片的结构定义, reflect.SliceHeader :
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
 





 定义方式
var (
a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3
d = c[:2] // 有2个元素的切片, len为2, cap为3
e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3
f = c[:0] // 有0个元素的切片, len为0, cap为3
g = make([]int, 3) // 有3个元素的切片, len和cap都为3
h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)和数组一样,内置的len()函数返回切片中有效元素的长度,内置的cap()函数返回切片容量的大小,容量必须大于或等于切片的长度。
 
遍历
for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d\n", i, c[i])
}在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息( reflect.SliceHeader ),并不会复制底层的数据。

添加
 
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包    注意,在容量不足的情况下,append()操作会导致重新分配内幕才能,可能导致巨大的内存分配和复制数据的代价。及时容量足够,依然需要用append()函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。 
删除切片元素

    根据要删除的元素的位置,有从开头位置删除、从中间位置删除和从尾部删除3种情况,其中删除切片尾部的元素最快:
a = []int{1, 2, 3}
a = a[:len(a) - 1] //删除尾部1个元素
a = a[:len(a) - n] //删除尾部n个元素 删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] //删除开头1个元素
a = a[N:] //删除开头N个元素
多维切片
 
同数组一样,切片也可以有多个维度。
package main

import (
"fmt"
)


func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}数据结果:C C++
JavaScript
Go Rust
更多参考文档:
https://www.jianshu.com/p/4eb3e490abe1
https://blog.51cto.com/14073476/2478276
https://cloud.tencent.com/developer/article/1354294
  查看全部
Go语言中数组、字符串和切片三者是密切相关的数据结构。这3种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。

Go语言的数据是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。

Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导师底层数据的复制。

切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数的传参时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。
 
数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值)。和数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片的工作原理还是要先理解数组。

定义方式
var a [3]int // 定义长度为3的int型数组, 元素全部为0
var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2} // 定义长度为3的int型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6
第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。

第二种方式定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。

第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。这种初始化方式和 map[int]Type 类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用0值初始化。

第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三第四个元素零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。
 数组与指针

Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如C语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。
var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针
fmt.Println(a[0], a[1]) // 打印数组的前2个元素
fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似
可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。内置函数 len 可以用于计算数组的长度, cap 函数可以用于计算数组的容量。不过对于数组类型来说, len 和 cap 函数返回的结果始终是一样的,都是对应数组类型的长度。

遍历
for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d\n", i, c[i])
}
用 for range 方式迭代的性能可能会更好一些,因为这种迭代可以保证不会出现数组越界的情形,每轮迭代对数组元素的访问时可以省去对下标越界的判断。

字符串

一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。

Go语言字符串的底层结构在 reflect.StringHeader 中定义:
type StringHeader struct {
Data uintptr
Len int
}
字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。

字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程。

字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问的同一块内存数据(因为字符串是只读的,相同的字符串面值常量通常是对应同一个字符串常量):
s := "hello, world"
hello := s[:5]
world := s[7:]
s1 := "hello, world"[:5]
s2 := "hello, world"[7:]
切片

切片(slice)是一种简化版的动态数组。切片的结构定义, reflect.SliceHeader :
type SliceHeader struct {
Data uintptr
Len int
Cap int
}

 

QQ截图20200608123151.jpg

 定义方式
var (
a []int // nil切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
c = []int{1, 2, 3} // 有3个元素的切片, len和cap都为3
d = c[:2] // 有2个元素的切片, len为2, cap为3
e = c[0:2:cap(c)] // 有2个元素的切片, len为2, cap为3
f = c[:0] // 有0个元素的切片, len为0, cap为3
g = make([]int, 3) // 有3个元素的切片, len和cap都为3
h = make([]int, 2, 3) // 有2个元素的切片, len为2, cap为3
i = make([]int, 0, 3) // 有0个元素的切片, len为0, cap为3
)
和数组一样,内置的len()函数返回切片中有效元素的长度,内置的cap()函数返回切片容量的大小,容量必须大于或等于切片的长度。
 
遍历
for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d\n", i, c[i])
}
在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息( reflect.SliceHeader ),并不会复制底层的数据。

添加
 
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
    注意,在容量不足的情况下,append()操作会导致重新分配内幕才能,可能导致巨大的内存分配和复制数据的代价。及时容量足够,依然需要用append()函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。 
删除切片元素

    根据要删除的元素的位置,有从开头位置删除、从中间位置删除和从尾部删除3种情况,其中删除切片尾部的元素最快:
a = []int{1, 2, 3}
a = a[:len(a) - 1] //删除尾部1个元素
a = a[:len(a) - n] //删除尾部n个元素
 删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] //删除开头1个元素
a = a[N:] //删除开头N个元素

多维切片
 
同数组一样,切片也可以有多个维度。
package main

import (
"fmt"
)


func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
数据结果:
C C++  
JavaScript
Go Rust

更多参考文档:
https://www.jianshu.com/p/4eb3e490abe1
https://blog.51cto.com/14073476/2478276
https://cloud.tencent.com/developer/article/1354294
 

#2020学习打卡##Go语言高级编程# 数组初始化时,索引初始化怎么理解?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 425 次浏览 • 2020-06-07 15:34 • 来自相关话题

如何在go项目中使用go mod?

回复

zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 589 次浏览 • 2020-05-22 17:09 • 来自相关话题

Golang:专属二进制编码方式Gob

zkbhj 发表了文章 • 0 个评论 • 402 次浏览 • 2020-05-20 11:25 • 来自相关话题

Gob 是 Go 自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding 包中找到。这种格式的数据简称为 Gob (即 Go binary 的缩写)。类似于 Python 的 “pickle” 和 Java 的 “Serialization”。

Gob 通常用于远程方法调用(RPCs,参见 15.9 的 rpc 包)参数和结果的传输,以及应用程序和机器之间的数据传输。 它和 JSON 或 XML 有什么不同呢?Gob 特定地用于纯 Go 的环境中,例如,两个用 Go 写的服务之间的通信。这样的话服务可以被实现得更加高效和优化。 Gob 不是可外部定义,语言无关的编码方式。因此它的首选格式是二进制,而不是像 JSON 和 XML 那样的文本格式。 Gob 并不是一种不同于 Go 的语言,而是在编码和解码过程中用到了 Go 的反射。

Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容。

只有可导出的字段会被编码,零值会被忽略。在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。当源数据类型增加新字段后,Gob 解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段。并且还提供了很大的灵活性,比如在发送者看来,整数被编码成没有固定长度的可变长度,而忽略具体的 Go 类型。
 例1:数据结构与bytes.Buffer之间的转换(编码成字节切片)package main

import (
"bytes"
"fmt"
"encoding/gob"
"io"
)

//准备编码的数据
type P struct {
X, Y, Z int
Name string
}

//接收解码结果的结构
type Q struct {
X, Y *int32
Name string
}

func main() {
//初始化一个数据
data := P{3, 4, 5, "CloudGeek"}
//编码后得到buf字节切片
buf := encode(data)
//用于接收解码数据
var q *Q
//解码操作
q = decode(buf)
//"CloudGeek": {3,4}
fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)

}

func encode(data interface{}) *bytes.Buffer {
//Buffer类型实现了io.Writer接口
var buf bytes.Buffer
//得到编码器
enc := gob.NewEncoder(&buf)
//调用编码器的Encode方法来编码数据data
enc.Encode(data)
//编码后的结果放在buf中
return &buf
}

func decode(data interface{}) *Q {
d := data.(io.Reader)
//获取一个解码器,参数需要实现io.Reader接口
dec := gob.NewDecoder(d)
var q Q
//调用解码器的Decode方法将数据解码,用Q类型的q来接收
dec.Decode(&q)
return &q
}例2:数据结构到文件的序列化和反序列化
package main

import (
"encoding/gob"
"os"
"fmt"
)

//试验用的数据类型
type Address struct {
City string
Country string
}

//序列化后数据存放的路径
var filePath string

func main() {
filePath = "./address.gob"
encode()
pa := decode()
fmt.Println(*pa) //{Chengdu China}
}

//将数据序列号后写到文件中
func encode() {
pa := &Address{"Chengdu", "China"}
//打开文件,不存在的时候新建
file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666)
defer file.Close()

//encode后写到这个文件中
enc := gob.NewEncoder(file)
enc.Encode(pa)
}

//从文件中读取数据并反序列化
func decode() *Address {
file, _ := os.Open(filePath)
defer file.Close()

var pa Address
//decode操作
dec := gob.NewDecoder(file)
dec.Decode(&pa)
return &pa
}参考文档:
https://blog.csdn.net/qq_34908844/article/details/80072905
https://www.cnblogs.com/cloudgeek/p/9348267.html
https://www.jianshu.com/p/5bac0f79626a
  查看全部
Gob 是 Go 自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding 包中找到。这种格式的数据简称为 Gob (即 Go binary 的缩写)。类似于 Python 的 “pickle” 和 Java 的 “Serialization”。

Gob 通常用于远程方法调用(RPCs,参见 15.9 的 rpc 包)参数和结果的传输,以及应用程序和机器之间的数据传输。 它和 JSON 或 XML 有什么不同呢?Gob 特定地用于纯 Go 的环境中,例如,两个用 Go 写的服务之间的通信。这样的话服务可以被实现得更加高效和优化。 Gob 不是可外部定义,语言无关的编码方式。因此它的首选格式是二进制,而不是像 JSON 和 XML 那样的文本格式。 Gob 并不是一种不同于 Go 的语言,而是在编码和解码过程中用到了 Go 的反射。

Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容。

只有可导出的字段会被编码,零值会被忽略。在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。当源数据类型增加新字段后,Gob 解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段。并且还提供了很大的灵活性,比如在发送者看来,整数被编码成没有固定长度的可变长度,而忽略具体的 Go 类型。
 例1:数据结构与bytes.Buffer之间的转换(编码成字节切片)
package main

import (
"bytes"
"fmt"
"encoding/gob"
"io"
)

//准备编码的数据
type P struct {
X, Y, Z int
Name string
}

//接收解码结果的结构
type Q struct {
X, Y *int32
Name string
}

func main() {
//初始化一个数据
data := P{3, 4, 5, "CloudGeek"}
//编码后得到buf字节切片
buf := encode(data)
//用于接收解码数据
var q *Q
//解码操作
q = decode(buf)
//"CloudGeek": {3,4}
fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)

}

func encode(data interface{}) *bytes.Buffer {
//Buffer类型实现了io.Writer接口
var buf bytes.Buffer
//得到编码器
enc := gob.NewEncoder(&buf)
//调用编码器的Encode方法来编码数据data
enc.Encode(data)
//编码后的结果放在buf中
return &buf
}

func decode(data interface{}) *Q {
d := data.(io.Reader)
//获取一个解码器,参数需要实现io.Reader接口
dec := gob.NewDecoder(d)
var q Q
//调用解码器的Decode方法将数据解码,用Q类型的q来接收
dec.Decode(&q)
return &q
}
例2:数据结构到文件的序列化和反序列化
package main

import (
"encoding/gob"
"os"
"fmt"
)

//试验用的数据类型
type Address struct {
City string
Country string
}

//序列化后数据存放的路径
var filePath string

func main() {
filePath = "./address.gob"
encode()
pa := decode()
fmt.Println(*pa) //{Chengdu China}
}

//将数据序列号后写到文件中
func encode() {
pa := &Address{"Chengdu", "China"}
//打开文件,不存在的时候新建
file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666)
defer file.Close()

//encode后写到这个文件中
enc := gob.NewEncoder(file)
enc.Encode(pa)
}

//从文件中读取数据并反序列化
func decode() *Address {
file, _ := os.Open(filePath)
defer file.Close()

var pa Address
//decode操作
dec := gob.NewDecoder(file)
dec.Decode(&pa)
return &pa
}
参考文档:
https://blog.csdn.net/qq_34908844/article/details/80072905
https://www.cnblogs.com/cloudgeek/p/9348267.html
https://www.jianshu.com/p/5bac0f79626a
 

Golang代码编程规范

zkbhj 发表了文章 • 0 个评论 • 369 次浏览 • 2020-04-01 17:36 • 来自相关话题

1、在 Go 代码上运行 gofmt 以自动修复大多数的机械性风格问题;
2、注释文档声明应该是完整的句子,以所描述事物的名称开头,并以句点结束;
3、Context 都是不可变的,因此可以将相同的 ctx 传递给多个共享相同截止日期,取消信号,安全凭据,跟踪等的调用;
4、如果 T 类型的方法与其指针类型 *T 相关联,请不要复制 T 类型的值;
5、不要使用包math/rand来生成密钥,请使用crypto/rand的 Reader 作为替代;
6、当声明一个空 slice 时,倾向于用var t string,nil slice 是首选的风格;
7、所有的顶级导出的名称都应该有 doc 注释,重要的未导出类型或函数声明也应如此;
8、不要将 panic 用于正常的错误处理。使用 error 和多返回值;
9、错误信息字符串不应大写(除非以专有名词或首字母缩略词开头)或以标点符号结尾;
10、添加新包时,请包含预期用法的示例:可运行的示例,或是演示完整调用链的简单测试;
11、当你生成 goroutines 时,要清楚它们何时或是否会退出(了解生命周期),请尽量让并发代码足够简单,从而更容易地确认 goroutine 的生命周期。否则容易产生内存泄漏等不可预知的问题;
12、不要使用 _ 变量丢弃 error。如果函数返回 error,请检查它以确保函数成功;
13、避免包重命名导入,防止名称冲突;好的包名称不需要重命名。如果发生命名冲突,则更倾向于重命名最接近本地的包或特定于项目的包。包导入按组进行组织,组与组之间有空行。标准库包始终位于第一组中;
14、除了特殊情况,不要在程序中使用 import .;
15、In-Band Errors,函数应返回一个附加值以指示其他返回值是否有效;
16、尝试将正常的代码路径保持在最小的缩进处,优先处理错误并缩进;
17、名称中的单词是首字母或首字母缩略词(例如 “URL” 或 “NATO” )需要具有相同的大小写规则,如如 “urlPony” 或 “URLPony”,而不是 “Url”;
18、Go 接口通常属于使用 interface 类型值的包,而不是实现这些值的包;
19、减少不必要的换行,如果行太长,可以更改名称或者语义,可能会起到很好的结果;类似的,如果函数太大,可能需要改变这个函数的功能边界,从而达到减小的目的;
20、混合大小写规则,导出常量大写字母开头,否则小写;
21、如果函数返回两个或三个相同类型的参数,那么在某些上下文中添加命名可能很有用,文档的清晰度总比在函数中的一行两行更重要;
22、包注释必须出现在 package 声明的临近位置,无空行;包注释的首字母必须大写;
23、包中名称的所有引用都将使用包名完成,因此您可以从标识符中省略该名称;
24、非必要是不必使用指针传递,使用值传递即可,除非是大型 struct类型或者是可能生长的小型 struct;
25、方法接收者的名称应该反映其身份;通常,其类型的一个或两个字母缩写就足够了,切使用上要保持一致,如果你在一个方法中叫将接收器命名为“c”,那么在其他方法中不要把它命名为“cl”;
26、在函数传参时,是使用值接收器还是使用指针接收器?几个标准和原则如下:
如果接收器是 map,func或 chan,则不要使用指向它们的指针。如果接收器是 slice 并且该方法不重新切片或不重新分配切片,则不要使用指向它的指针。如果该方法需要改变接收器的值,则接收器必须是指针。如果接收器是包含 sync.Mutex 或类似同步字段的 struct,则接收器必须是避免复制的指针。如果接收器是大型结构或数组,则指针接收器更有效。多大才算大?假设它相当于将其包含的所有元素作为参数传递给方法。如果感觉太大,那么对接收器来说也太大了。函数或方法可以改变接收器吗(并发调用或调用某方法时继续调用相关方法或函数)?在调用方法时,值类型会创建接收器的副本,因此外部更新将不会应用于此接收器。如果必须在原始接收器中看到更改效果,则接收器必须是指针。如果接收器是 struct,数组或 slice,并且其任何元素是指向可能改变的对象的指针,则更倾向于使用指针接收器,因为它将使读者更清楚地意图。如果接收器是一个小型数组或 struct,那么它自然是一个值类型(例如,类似于time.Time类型),对于没有可变字段,没有指针的类型,或者只是一个简单的基本类型,如 int 或 string,值接收器是合适的。值接收器可以减少可以生成的垃圾量;如果将值作为参数传递给值类型方法,则可以使用堆栈上的副本而不需要在堆上进行分配。(编译器试图避免这种分配,但它不能总是成功)因此,在没有进行分析之前,不要选择值接收器类型。最后,如有疑问,请使用指针接收器。
27、相比异步函数更倾向于同步函数——直接返回结果的函数,或是在返回之前已完成所有回调或 channel 操作的函数。尽量减少异步函数的使用。同步函数让 goroutine 在调用中本地化,能够更容易地推断其生命周期并避免泄漏和数据竞争;
28、在任何情况下,你都有责任向可能会在将来调试你的代码的开发者提供有用的消息;
29、Go 中的变量名称应该短而不是长,尤其是范围域内的局部变量。基本规则:范围域中,越晚使用的变量,名称必须越具有描述性。对于方法接收器,一个或两个字母就足够了。
 
原文链接:https://github.com/golang/go/w ... ments 查看全部
1、在 Go 代码上运行 gofmt 以自动修复大多数的机械性风格问题;
2、注释文档声明应该是完整的句子,以所描述事物的名称开头,并以句点结束;
3、Context 都是不可变的,因此可以将相同的 ctx 传递给多个共享相同截止日期,取消信号,安全凭据,跟踪等的调用;
4、如果 T 类型的方法与其指针类型 *T 相关联,请不要复制 T 类型的值;
5、不要使用包math/rand来生成密钥,请使用crypto/rand的 Reader 作为替代;
6、当声明一个空 slice 时,倾向于用var t string,nil slice 是首选的风格;
7、所有的顶级导出的名称都应该有 doc 注释,重要的未导出类型或函数声明也应如此;
8、不要将 panic 用于正常的错误处理。使用 error 和多返回值;
9、错误信息字符串不应大写(除非以专有名词或首字母缩略词开头)或以标点符号结尾;
10、添加新包时,请包含预期用法的示例:可运行的示例,或是演示完整调用链的简单测试;
11、当你生成 goroutines 时,要清楚它们何时或是否会退出(了解生命周期),请尽量让并发代码足够简单,从而更容易地确认 goroutine 的生命周期。否则容易产生内存泄漏等不可预知的问题;
12、不要使用 _ 变量丢弃 error。如果函数返回 error,请检查它以确保函数成功;
13、避免包重命名导入,防止名称冲突;好的包名称不需要重命名。如果发生命名冲突,则更倾向于重命名最接近本地的包或特定于项目的包。包导入按组进行组织,组与组之间有空行。标准库包始终位于第一组中;
14、除了特殊情况,不要在程序中使用 import .;
15、In-Band Errors,函数应返回一个附加值以指示其他返回值是否有效;
16、尝试将正常的代码路径保持在最小的缩进处,优先处理错误并缩进;
17、名称中的单词是首字母或首字母缩略词(例如 “URL” 或 “NATO” )需要具有相同的大小写规则,如如 “urlPony” 或 “URLPony”,而不是 “Url”;
18、Go 接口通常属于使用 interface 类型值的包,而不是实现这些值的包;
19、减少不必要的换行,如果行太长,可以更改名称或者语义,可能会起到很好的结果;类似的,如果函数太大,可能需要改变这个函数的功能边界,从而达到减小的目的;
20、混合大小写规则,导出常量大写字母开头,否则小写;
21、如果函数返回两个或三个相同类型的参数,那么在某些上下文中添加命名可能很有用,文档的清晰度总比在函数中的一行两行更重要;
22、包注释必须出现在 package 声明的临近位置,无空行;包注释的首字母必须大写;
23、包中名称的所有引用都将使用包名完成,因此您可以从标识符中省略该名称;
24、非必要是不必使用指针传递,使用值传递即可,除非是大型 struct类型或者是可能生长的小型 struct;
25、方法接收者的名称应该反映其身份;通常,其类型的一个或两个字母缩写就足够了,切使用上要保持一致,如果你在一个方法中叫将接收器命名为“c”,那么在其他方法中不要把它命名为“cl”;
26、在函数传参时,是使用值接收器还是使用指针接收器?几个标准和原则如下:
  • 如果接收器是 map,func或 chan,则不要使用指向它们的指针。如果接收器是 slice 并且该方法不重新切片或不重新分配切片,则不要使用指向它的指针。
  • 如果该方法需要改变接收器的值,则接收器必须是指针。
  • 如果接收器是包含 sync.Mutex 或类似同步字段的 struct,则接收器必须是避免复制的指针。
  • 如果接收器是大型结构或数组,则指针接收器更有效。多大才算大?假设它相当于将其包含的所有元素作为参数传递给方法。如果感觉太大,那么对接收器来说也太大了。
  • 函数或方法可以改变接收器吗(并发调用或调用某方法时继续调用相关方法或函数)?在调用方法时,值类型会创建接收器的副本,因此外部更新将不会应用于此接收器。如果必须在原始接收器中看到更改效果,则接收器必须是指针。
  • 如果接收器是 struct,数组或 slice,并且其任何元素是指向可能改变的对象的指针,则更倾向于使用指针接收器,因为它将使读者更清楚地意图。
  • 如果接收器是一个小型数组或 struct,那么它自然是一个值类型(例如,类似于time.Time类型),对于没有可变字段,没有指针的类型,或者只是一个简单的基本类型,如 int 或 string,值接收器是合适的。值接收器可以减少可以生成的垃圾量;如果将值作为参数传递给值类型方法,则可以使用堆栈上的副本而不需要在堆上进行分配。(编译器试图避免这种分配,但它不能总是成功)因此,在没有进行分析之前,不要选择值接收器类型。
  • 最后,如有疑问,请使用指针接收器。

27、相比异步函数更倾向于同步函数——直接返回结果的函数,或是在返回之前已完成所有回调或 channel 操作的函数。尽量减少异步函数的使用。同步函数让 goroutine 在调用中本地化,能够更容易地推断其生命周期并避免泄漏和数据竞争;
28、在任何情况下,你都有责任向可能会在将来调试你的代码的开发者提供有用的消息;
29、Go 中的变量名称应该短而不是长,尤其是范围域内的局部变量。基本规则:范围域中,越晚使用的变量,名称必须越具有描述性。对于方法接收器,一个或两个字母就足够了。
 
原文链接:https://github.com/golang/go/w ... ments

Golang学习:Go goroutine理解

zkbhj 发表了文章 • 0 个评论 • 421 次浏览 • 2019-12-19 16:10 • 来自相关话题

Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。

为了更好理解Goroutine,现讲一下线程和协程的概念

线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。

协程(coroutine):又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。

和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。

Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。package main

import "fmt"

func main() {

messages := make(chan string)

go func() { messages <- "ping" }()

msg := <-messages
fmt.Println(msg)
}
先给个简单实例
func loop() {
for i := 0; i < ; i++ {
fmt.Printf("%d ", i)
}
}

func main() {
go loop() // 启动一个goroutine
loop()
}GO并发的实现原理

一、Go并发模型

Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。

CSP并发模型是在1970年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。

请记住下面这句话:
DO NOT COMMUNICATE BY SHARING MEMORY; INSTEAD, SHARE MEMORY BY COMMUNICATING.
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”

普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go中也实现了传统的线程并发模型。

Go的CSP并发模型,是通过goroutine和channel来实现的。

goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

生成一个goroutine的方式非常的简单:Go一下,就生成了。
go f();通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。

在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

示例如下:
package main

import "fmt"

func main() {

messages := make(chan string)

go func() { messages <- "ping" }()

msg := <-messages
fmt.Println(msg)
}注意 main()本身也是运行了一个goroutine。

messages:= make(chan int) 这样就声明了一个阻塞式的无缓冲的通道

chan 是关键字 代表我要创建一个通道。
 
GO并发模型的实现原理

我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。

我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。

线程模型的实现,可以分为以下几种方式:

用户级线程模型





 
如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。它可以做快速的上下文切换。缺点是不能有效利用多核CPU。
 
内核级线程模型





 
这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。一个用户态的线程对应一个系统线程,它可以利用多核机制,但上下文切换需要消耗额外的资源。C++就是这种。
 
两级线程模型





 
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。

M个用户线程对应N个系统线程,缺点增加了调度器的实现难度。

Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)。
 
Go线程实现模型MPG
 
M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。
G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。

P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。

三者关系如下图所示:





 
以上这个图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个上下文连接一个或者多个Goroutine。为了运行goroutine,线程必须保存上下文。

上下文P(Processor)的数量在启动时设置为GOMAXPROCS环境变量的值或通过运行时函数GOMAXPROCS()。通常情况下,在程序执行期间不会更改。上下文数量固定意味着只有固定数量的线程在任何时候运行Go代码。我们可以使用它来调整Go进程到个人计算机的调用,例如4核PC在4个线程上运行Go代码。

图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues。

Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。





 
抛弃P(Processor)

你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的,是让我们可以直接放开其他线程,当遇到内核线程阻塞的时候。

一个很简单的例子就是系统调用sysall,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine被调度执行。





 
如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能来自线程缓存),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行Goroutine队列中的其他Goroutine。

当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,将自己置于线程缓存中并进入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。

均衡的分配工作

按照以上的说法,上下文P会定期的检查全局的goroutine 队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。

每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。
该如何解决呢?

Go的做法倒也直接,从其他P中偷一半!





 
Goroutine 小结

优点:

1、开销小

POSIX的thread API虽然能够提供丰富的API,例如配置自己的CPU亲和性,申请资源等等,线程在得到了很多与进程相同的控制权的同时,开销也非常的大,在Goroutine中则不需这些额外的开销,所以一个Golang的程序中可以支持10w级别的Goroutine。

每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine:2KB ,线程:8MB)

2、调度性能好

在Golang的程序中,操作系统级别的线程调度,通常不会做出合适的调度决策。例如在GC时,内存必须要达到一个一致的状态。在Goroutine机制里,Golang可以控制Goroutine的调度,从而在一个合适的时间进行GC。

在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。

缺点:

协程调度机制无法实现公平调度。
 
参考文档:https://segmentfault.com/a/1190000018150987 查看全部
Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。

为了更好理解Goroutine,现讲一下线程和协程的概念

线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。

协程(coroutine):又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。

和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。

Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。
package main

import "fmt"

func main() {

messages := make(chan string)

go func() { messages <- "ping" }()

msg := <-messages
fmt.Println(msg)
}

先给个简单实例
func loop() {
for i := 0; i < ; i++ {
fmt.Printf("%d ", i)
}
}

func main() {
go loop() // 启动一个goroutine
loop()
}
GO并发的实现原理

一、Go并发模型

Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型

CSP并发模型是在1970年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。

请记住下面这句话:
DO NOT COMMUNICATE BY SHARING MEMORY; INSTEAD, SHARE MEMORY BY COMMUNICATING.
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”

普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go中也实现了传统的线程并发模型。

Go的CSP并发模型,是通过goroutine和channel来实现的。

goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

生成一个goroutine的方式非常的简单:Go一下,就生成了。
go f();
通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。

在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

示例如下:
package main

import "fmt"

func main() {

messages := make(chan string)

go func() { messages <- "ping" }()

msg := <-messages
fmt.Println(msg)
}
注意 main()本身也是运行了一个goroutine。

messages:= make(chan int) 这样就声明了一个阻塞式的无缓冲的通道

chan 是关键字 代表我要创建一个通道。
 
GO并发模型的实现原理

我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。

我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。

线程模型的实现,可以分为以下几种方式:

用户级线程模型

341325984-5c6510603f05b_articlex.jpg

 
如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。它可以做快速的上下文切换。缺点是不能有效利用多核CPU。
 
内核级线程模型

1778856042-5c65105d351b2_articlex.jpg

 
这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。一个用户态的线程对应一个系统线程,它可以利用多核机制,但上下文切换需要消耗额外的资源。C++就是这种。
 
两级线程模型

2493845633-5c65105970131_articlex.jpg

 
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。

M个用户线程对应N个系统线程,缺点增加了调度器的实现难度。

Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)。
 
Go线程实现模型MPG
 
M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。
G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。

P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。

三者关系如下图所示:

1380329424-5c65106263b83_articlex.jpg

 
以上这个图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个上下文连接一个或者多个Goroutine。为了运行goroutine,线程必须保存上下文。

上下文P(Processor)的数量在启动时设置为GOMAXPROCS环境变量的值或通过运行时函数GOMAXPROCS()。通常情况下,在程序执行期间不会更改。上下文数量固定意味着只有固定数量的线程在任何时候运行Go代码。我们可以使用它来调整Go进程到个人计算机的调用,例如4核PC在4个线程上运行Go代码。

图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues。

Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。

2866224357-5c6510c9cc2fe_articlex.jpg

 
抛弃P(Processor)

你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的,是让我们可以直接放开其他线程,当遇到内核线程阻塞的时候。

一个很简单的例子就是系统调用sysall,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine被调度执行。

3568025728-5c65105a25fa0_articlex.jpg

 
如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能来自线程缓存),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行Goroutine队列中的其他Goroutine。

当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,将自己置于线程缓存中并进入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。

均衡的分配工作

按照以上的说法,上下文P会定期的检查全局的goroutine 队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。

每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。
该如何解决呢?

Go的做法倒也直接,从其他P中偷一半!

357488702-5c65105c93f2d_articlex.jpg

 
Goroutine 小结

优点:

1、开销小

POSIX的thread API虽然能够提供丰富的API,例如配置自己的CPU亲和性,申请资源等等,线程在得到了很多与进程相同的控制权的同时,开销也非常的大,在Goroutine中则不需这些额外的开销,所以一个Golang的程序中可以支持10w级别的Goroutine。

每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine:2KB ,线程:8MB)

2、调度性能好

在Golang的程序中,操作系统级别的线程调度,通常不会做出合适的调度决策。例如在GC时,内存必须要达到一个一致的状态。在Goroutine机制里,Golang可以控制Goroutine的调度,从而在一个合适的时间进行GC。

在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。

缺点:

协程调度机制无法实现公平调度。
 
参考文档:https://segmentfault.com/a/1190000018150987