GOlang

被誉为替代Java的新一代网络语言,的确很好。暂时先写一个简单的流程,有些可能先列出提纲。很早就学习了,学习笔记在evernote上写了不少,感觉还是整理放在github上,和大家一起学习。

常用传送门

  • 官方文档
  • 推荐书&参考文档
    • 官方在线教程。https://go-zh.org
    • The Go programming language
    • Effective Go
    • 本地api查看, 命令行运行 godoc -http=:8080 然后浏览器打开

IDE

  • Intellij Idea 安装Go的插件
  • eclipse 安装插件https://github.com/GoClipse/releases/raw/master/
  • http://www.golangtc.com/download/liteide

安装

  • 下载
  • 设置环境路径

代码风格

  • { 的位置
  • 骆驼命名。
  • 直接执行go fmt *.go进行代码的格式化。
  • 目录结构

语法

  • 在 Go 中,首字母大写的名称是被导出的,小写隐藏。
  • var 语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。var i int
  • 变量定义可以包含初始值,每个变量对应一个。var i, j int = 1, 2

    类型

整形

有int8、int16、int32和int64四种截然不同大小的有符号整形数类型,分别对应8、16、32、64bit大小的有符号整形数,与此对应的是uint8、uint16、uint32和uint64四种无符号整形数类型。因此你的运算可能会溢出。

1
2
3
4
5
	//overflow
	var u8 uint8 = 255
	fmt.Println(u8,u8+1,u8+2)	
	var u  = 255
	fmt.Println(u,u+1,u+2)
  • 位操作

Go语言还提供了以下的bit位操作运算符.
位操作运算符&^用于按位置零(AND NOT):表达式z = x &^ y结果z的bit位为0,如果对应y中bit位为1的话,否则对应的bit位等于x相应的bit位的值。

1
2
3
4
5
6
7
8
9
10
11
 
	//bit operator
	var x uint8 = 1<<1 | 1<<5
	var y uint8 = 1<<1 | 1<<2
	fmt.Printf("%08b\n", x)
	fmt.Printf("%08b\n", y)
	fmt.Println("")
	fmt.Printf("%08b\n", x&y) //位运算 AND
	fmt.Printf("%08b\n", x|y) //位运算 OR
	fmt.Printf("%08b\n", x^y) //位运算 XOR
	fmt.Printf("%08b\n", x&^y) // 位清空 (AND NOT)
  • 8 16 10进制的打印
    可以用%d、%o或%x参数控制输出的进制格式
    字符使用%c参数打印,或者是用%q参数打印带单引号的字符.
1
2
3
4
5
6
7
 
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)

//%之后的[1]副词告诉Printf函数再次使用第一个操作数。第二,%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。

浮点数

复数

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

字符串

一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。

内置的len函数可以返回一个字符串中的字节数目。
一个原生的字符串面值形式 xxxx,使用反引号代替双引号。
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

string 字符串的内容可以用类似于数组下标的方式获取,但与数组不同,字符串的内容不能在初始化后被修改。
- 支持中文。

1
2
3
4
5
6
    str := "hello中国"
    fmt.Println("len:",len(str))
    for i,ch := range str {
        fmt.Println(i,string(ch))
     }
     

常量

每种常量的潜在类型都是基础类型:boolean、string或数字。它们的值是在编译期就确定的。

  • 常量的定义与变量类似,只不过使用 const 关键字。
  • 常量可以是字符、字符串、布尔或者类型的值。
    常量不能使用 := 语法定义。
  • 定义常量组的时候,如果不提供初始值,则表示使用上行的表达式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const
(
	a = 1
	b
	c
)

const
(
	d, e = 1,2
	f, g
)

func main() {
 fmt.Println(a, b, c)
 fmt.Println(d,e,f,g)
}

//输出 1 1 1 .
//输出 1 2 1 2
 

iota 常量生成器

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

  • 预定义常量
    • true false itoa .itoa 下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。她和出现的位置有关。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const
(
	aa = 2
	bb = iota
	cc = iota
	dd = 0
	ee = iota
)

const
(
	cccc = 100
	bbbb = iota
)

func main() {

	fmt.Println(a, b, c)
	fmt.Println(d,e,f,g)
	fmt.Println(aa,bb,cc,dd,ee,bbbb)
}

//2 1 2 0 4 1


//test2
const(
	B float64 = 1 <<(iota*10)
	KB
	MB
)
func main() {I
	fmt.Println(B,KB,MB) //1 1024 1.048576e+06
}
  • 与 C 不同的是 Go 的在不同类型之间的项目赋值时需要显式转换。
类型推导
- 在定义一个变量却并不显式指定其类型时(使用 := 语法或者 var = 表达式语法), 变量的类型由(等号)右侧的值推导得出。 - 类型的转换。必须显式声明。Type(vale)  
 a := int(b)

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组

  • 类型 [n]T 是一个有 n 个类型为 T 的值的数组。 var a[10] int
  • 在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算
  • 数组的长度是其类型的一部分,因此数组不能改变大小。
  • 数组可以进行 == !=的比较
  • 指定一个索引和对应值列表的方式初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"
r := [...]int{99: -1}
1
2
3
4
5
6
7
8
9
10
//对最后一个数组设置值,其他默认值。
a:=[4]int{3:10}
fmt.Println(a) //[0 0 0 10]
b:=[...]int{3:10,6:5}
fmt.Println(b) //[0 0 0 10 0 0 5]
	
//new一个数组
p := new ([10]int)
p[2] =2
fmt.Println(p) //&[0 0 2 0 0 0 0 0 0 0]

Sice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

  • 一个 slice 会指向一个序列的值,并且包含了长度信息。 s := []int{2, 3, 5, 7, 11, 13}
  • slice 可以重新切片,创建一个新的 slice 值指向相同的数组。 s[1:4]
  • slice 由函数 make 创建。这会分配一个全是零值的数组并且返回一个 slice 指向这个数组:
  • a := make([]int, 5) // len(a)=5
  • slice 的零值是 nil 。一个 nil 的 slice 的长度和容量是 0。var z []int
  • 向 slice 添加元素是一种常见的操作
  • append(a, 2, 3, 4) 返回一个新的数组。

Map

哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value.一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型.其中K对应的key必须是支持==比较运算符的数据类型

内置的make函数可以创建一个map:

1
2
3
4
5
6
7
8
	   
ages := make(map[string]int)

//test2 map字面值的语法创建map,同时还可以指定一些最初的key/value
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}
  • map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作.禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
  • 可以用range进行遍历map中全部的key/value。
  • 判断是否key存在。
    if age, ok := ages["bob"]; !ok { /* ... */ }
    
  • map 在使用之前必须用 make 而不是 new 来创建;值为 nil 的 map 是空的,并且不能赋值。

Struct

  • 一个结构体( struct )就是一个字段的集合。
  • 结构体字段使用点号来访问。
  • 结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。
1
2
3
4
5
	   
type Vertex struct {
     X, Y int
     
v2 = Vertex{X: 1}  // Y:0 被省略   //{1 0 }
  • 组合,匿名组合
    • 可见性。
    • 可以访问的以大写字母开头。
    • 可见性是包一级的,而不是类型一级的。
  • Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	   
type Point struct {
    X, Y int
}

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

//ok
w = Wheel{Circle{Point{8, 8}, 5}, 20}

//errors
w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此上面的语句都不能编译通过.

  • 如果嵌入的匿名的结构体有相同的值,就需要通过她们的名称来访问了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	   
package main
import "fmt"

type Point struct {
	X int
	Y int
}

type Vector struct {
	X int
	Y int
}

type Component struct {
	Point
	Vector
}

func main()  {
	var t1 = Component{Point{1,2},Vector{3,4}}
	fmt.Println(t1,t1.Point.X,t1.Point.Y,t1.Vector.X,t1.Vector.Y)
	fmt.Println(t1.X) //二义性错误。
}

JSON

  • Go语言的大多数数据类型都可以 化为有 的JSON文本
    • 如果转化前的数据结构中出现 ,那么将会 转化所指向的值,如果指向的是指针值, 那么null将作为结果
    • encode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	   

type DD struct {
	Age int
	Name string
	}
type Book struct  {
Title string
Author []string
Publisher string
IsPublisher bool
Price float32
More *DD
}
func main() {
    d := DD{20,"DD"}
    gobook := Book{"Go lang",
       []string{"111","222"},
       "Publ1",
       true,
       32.2,&d}
    b, err := json.Marshal(gobook)

    if err != nil {
       fmt.Println("Parse error")
    }
   fmt.Println(string(b))
}

-decode

1
2
3
4
5
6
7
8
9
10
11
 
 json.Unmarshal( )
//解码未知的Json结构。返回的是一个map[string]interface{}的结构
//解析未知结构的Json
book2 := []byte(`{"Title":"Go lang","Author":["333","222"],"Publisher":"Publ1"}`)
 var b2 interface{}
 if err = json.Unmarshal(book2,&b2);err ==nil{
    fmt.Println("unkonwn:",b2)
 }else {
    fmt.Println("xxxx",err.Error())
 }

函数

函数声明:

1
2
3
4
5
6
func name(parameter-list) (result-list) {
    body
}

//打印函数
fmt.Printf("%T\n", add)   // "func(int, int) int"
  • 一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型.
  • 小写字母开头的函数只在本包内可见,大写字母开头的函数才 能被其他包使用。
  • 不定参数 func myFunc(args … int)
  • 任意类型的不定参数 func Printf(format string,args … interface{}){}
  • 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
1
2
3
4
     func add(x, y int) int {
        return x + y
     }    
	
  • 函数可以返回任意数量的返回值。
1
2
3
4
     func swap(x, y string) (string, string) {
         return y, x
     }
     
  • 在函数中, := 简洁赋值语句在明确类型的地方,可以用于替代 var 定义。函数外的每个语句都必须以关键字开始( var 、 func 、等等), := 结构不能使用在函数外。
  • 错误。对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。
  • 函数可以与nil比较。函数值之间是不可比较的,也不能用函数值作为map的key。
  • 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sum(vals...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}



func errorf(linenum int, format string, args ...interface{}) {
    fmt.Fprintf(os.Stderr, "Line %d: ", linenum)
    fmt.Fprintf(os.Stderr, format, args...)
    fmt.Fprintln(os.Stderr)
}
linenum, name := 12, "count"
errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
  • Deferred函数.

一些提示

  • Name
    • Package Name
    • The importer of a package will use the name to refer to its contents, so exported names in the package can use that fact to avoid stutter. (Don’t use the import . notation, which can simplify tests that must run outside the package they are testing, but should otherwise be avoided.)
  • Data
    • new

      • unlike its namesakes in some other languages it does not initialize the memory, it only zeros it.
      • That is, new(T) allocates zeroed storage for a new item of type T and returns its address, a value of type *T.
    • make
      • The built-in function make(T, args) serves a purpose different from new(T). It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T).
    • Remember that make applies only to maps, slices and channels and does not return a pointer. To obtain an explicit pointer allocate with new or take the address of a variable explicitly.
    • Map

      • The key can be of any type for which the equality operator is defined, such as integers, floating point and complex numbers, strings, pointers, interfaces (as long as the dynamic type supports equality), structs and arrays.
      • Slices cannot be used as map keys, because equality is not defined on them. Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.
      • map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作.
        -
        _ = &ages[“bob”] // compile error: cannot take address of map element

      • 禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
1
2
3
4
5
6
	
var v,ok = m[20]
fmt.Println(ok,v)
m[20]++   //危险。因为m[20]原来可以不存在,返回的可能是默认值,现在进行++会进行了赋值
v,ok = m[20]
fmt.Println(ok,v)

控制语句

  • Switch 没有条件的 switch 同 switch true 一样。没有break,满足条件就执行对应的块。
  • 这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。
  • defer 语句会延迟函数的执行直到上层函数返回。
    延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。
  • defer 延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用

指针

  • 类型 *T 是指向类型 T 的值的指针。其零值是 nil 。
  • & 符号会生成一个指向其作用对象的指针。
  • 函数的闭包
    • Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。

方法

  • Go没有类。
  • 仍然可以在结构体类型上定义方法。方法接收者 出现在 func 关键字和方法名之间的参数中。在函数声明时,在其名字之前放上一个变量,即是一个方法
1
2
3
4
5
6
 type Vertex struct {
     X, Y float64
 }
 func (v *Vertex) Abs() float64 {
     return math.Sqrt(v.X*v.X + v.Y*v.Y)
 }
  • 只有类型(Point)和指向他们的指针(*Point),才是可能会出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的
1
2
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
  • 通过嵌入结构体来扩展类型。
  • 封装。大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。
  • 一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。

interface

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

  • 接口类型是由一组方法定义的集合。
  • 接口类型的值可以存放实现这些方法的任何值。
  • 接口类型可以组合。
  • Any 类型。interface{}可以指向任意对象的Any类型。
  • type assertion

     - value.(typename)
     - the result is a new value with the static type typeName  - unused import and variables
    
  • 怎么反向知道这个interface变量里面实际保存了的是哪个类型的对象?一般两种方法
    • Comma-ok断言
      Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
    • switch测试 element.(type)语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用comma-ok。
1
2
3
4
5
6
7
8
9
10
   switch value := element.(type) {
            case int:
                fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
            case string:
                fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
            case Person:
                fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
            default:
                fmt.Println("list[%d] is of a different type", index)
        }

反射

Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
t := reflect.TypeOf(i)    //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i)   //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

tag := t.Elem().Field(0).Tag  //获取定义在struct里面的标签
name := v.Elem().Field(0).String()  //获取存储在第一个字段里面的值

//demo
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

//射的字段必须是可修改的。修改值,传引用
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

gorountine

  • goroutine 是由 Go 运行时环境管理的轻量级线程。当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go.

channel

一个channels是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int. channel的零值也是nil.两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真.

  • channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值。 写入数据 ch <- value 读取数据 value :<- ch
  • 当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值。所以发送或者接收的时候,需要对channel进行判断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import (
	"fmt"
// "time"
// "hello"
)


func g1(c chan int, t chan int) {

	fmt.Println("g1")
	for i := 0; i < 4; i++ {
		c <- int(i * 10)
	}
	close(c)
	fmt.Println("close g1")
	t <- 1
}

func g2(c chan int, t chan int) {

	for i := 0; i < 2; i++ {
		c <- i
	}
	close(c)
	fmt.Println("close g2")
	t <- 1
}

func main() {

	d1 := make(chan int)
	d2 := make(chan int)
	tag := make(chan int, 2)
	go g1(d1, tag)
	go g2(d2, tag)

	count := 0
	for {
		select {
		case gch1, ok := <-d1:
			if ok {
				fmt.Println("d1", gch1)
			}

		case gch2, ok := <-d2:
			if ok {
				fmt.Println("d2", gch2)
			}

		case i, ok := <-tag:
			if ok {
				count = i + count
				fmt.Println("index", i, count)
				if count == 2 {
					fmt.Println("Over", i)
					return
				}
			}
		}
	}

}

output因为是并发可能每个人执行的不一样但是结果应该如此
d2 0
d2 1
close g2
index 1 1
g1
d1 0
d1 10
d1 20
d1 30
close g1
index 1 2
Over 1

不带缓存的Channels

调用make函数创建的时一个无缓存的channel,但是我们也可以指定第二个整形参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

  • 不带缓存的Channels.一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。

基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。

在讨论并发编程时,当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。

当我们说x事件既不是在y事件之前发生也不是在y事件之后发生,我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的,我们只是不能确定这两个事件发生的先后顺序。

只要当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的使用调用对应的Close方法来关闭文件。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import "fmt"

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	// Counter
	go func() {
		for x := 0; x < 10; x++ {
			naturals <- x
		}

		close(naturals)
	}()

	// Squarer
	go func() {
		for x:=range naturals {
				squares <- x * x
			}
		close(squares)
	}()

	// Printer (in main goroutine)
	for x := range squares {
		fmt.Println(x)
	}
}

带缓存的Channels

  • 单方向的Channel
    • var ch2 cha<- int //只能写入int
    • 初始化 ch4 := make(chan int) ch5 := <-chan int(chr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	
	func counter(out chan<- int) {
    for x := 0; x < 100; x++ {
        out <- x
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for v := range in {
        out <- v * v
    }
    close(out)
}

func printer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
}

func main() {
    naturals := make(chan int)
    squares := make(chan int)
    go counter(naturals)
    go squarer(squares, naturals)
    printer(squares)
}
  • select
    • 每个case语句中,必须是一个IO操作。
    • 只要一个case已经完成,程序就会继续往下执行,而不会考虑其他的case的情况。
    • 关闭 close()测试是否关闭 x,ok := <- ch
    • 多核并行化
    • 全局唯一性操作 sync.Once

基于共享变量的并发

当我们能够没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话,就说明x和y这两个事件是并发的。

一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,如果这个类型是并发安全的话,那么所有它的访问方法和操作就都是并发安全的。

并发安全的类型是例外,而不是规则,所以只有当文档中明确地说明了其是并发安全的情况下,你才可以并发地去访问它。导出包级别的函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。

一个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。

竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。竞争条件是很恶劣的一种场景,因为这种问题会一直潜伏在你的程序里,然后在非常少见的时候蹦出来,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。

数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义,有三种方式可以避免数据竞争:

  • 第一种方法是不要去写变量。如果我们在创建goroutine之前的初始化阶段,就初始化了map中的所有条目并且再也不去修改它们,那么任意数量的goroutine并发访问Icon都是安全的,因为每一个goroutine都只是去读取而已。
  • 第二种避免数据竞争的方法是,避免从多个goroutine访问变量.
    由于其它的goroutine不能够直接访问变量,它们只能使用一个channel来发送给指定的goroutine请求来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信;使用通信来共享数据”。
  • 第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”.Mutex…..保证最多只有一个goroutine在同一时刻访问一个共享变量。

sync.Mutex互斥锁

保证最多只有一个goroutine在同一时刻访问一个共享变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
	 
 import "sync"

var (
    mu      sync.Mutex // guards balance
    balance int
)

func Deposit(amount int) {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

func Balance() int {
    mu.Lock()
    b := balance
    mu.Unlock()
    return b
}

//使用defer
func Balance() int {
    mu.Lock()
    defer mu.Unlock()
    return balance
}


 
sync.RWMutex读写锁

允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex.

RWMutex只有当获得锁的大部分goroutine都是读操作,而锁在竞争条件下,也就是说,goroutine们必须等待才能获取到锁的时候,RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。.

1
2
3
4
5
6
7
8
9
	
 
 var mu sync.RWMutex
var balance int
func Balance() int {
    mu.RLock() // readers lock
    defer mu.RUnlock()
    return balance
}
内存同步

在现代计算机中可能会有一堆处理器,每一个都会有其本地缓存(local cache)。为了效率,对内存的写入一般会在每一个处理器中缓冲,并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit

在一个独立的goroutine中,每一个语句的执行顺序是可以被保证的;也就是说goroutine是顺序连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时,我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。

所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。

sync.Once初始化

使用互斥访问icons的代价就是没有办法对该变量进行并发访问,即使变量已经被初始化完毕且再也不会进行变动。

sync.Once。概念上来讲,一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;互斥量用来保护boolean变量和客户端数据结构。Do这个唯一的方法需要接收初始化函数作为其参数

1
2
3
4
5
6
7
8
9
	

var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}
竞争条件检测

Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。

只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。

竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你到包。

工程管理

  • 跨平台开发
  • 单元测试
    • _test.go结尾
    • 功能测试&性能测试
      • Testxx (t *testing.T)
      • Benchmarkxxx(t *testing.T)

语言交互

  • 调用C
    • import 需要紧跟头文件注释。

部署&交叉编译

  • 主要设置 GOOS=linux GOARCH=amd64
  • GOOS=linux GOARCH=amd64 revel build github.com/revel/samples/chat ~/Desktop/chat

常用的系统及架构

1
2
3
4
5
6
7
8
9
10
 
$GOOS      $GOARCH
darwin      386
darwin      amd64
freebsd      386
freebsd      amd64
linux      386
linux      amd64
linux      arm      incomplete
windows      386      incomplete