go指南 - 官网
官网的文档,应该是最好的入门了吧
基础
每个 Go 程序都是由包构成的。
程序从 main
包开始运行。
一般包名与导入路径的最后一个元素一致。例如,"math/rand"
包中的源码均以 package rand
语句开始。
import (
"fmt"
"math"
)
// 上面这种导入方式更好
import "fmt"
import "math"
如果一个名字以大写字母开头,那么它就是已导出的
如,math.pi
就是未导出的,就没有定义,不能访问和调用。
math.Pi
就导出了。导入一个包,只能引用已经导出的名字。
类型在变量名之后。
c的类型是定义在前面的。如果函数的参数是函数,或者返回值是函数,那么就会难以理解
int (*fp)(int (*ff)(int x, int y), int b) int (*(*fp)(int (*)(int, int), int))(int, int)
go从后置类型的定义进化而来
x: int p: pointer to int a: array[3] of int 变成 x int p *int a [3]int
用go写就是
f func(func(int,int) int, int) int f func(func(int,int) int, int) func(int, int) int
但是指针的*是前置的
var p *int x = *p // 而不是 var p *int x = p*
因为可能被理解为乘法
变量初始化如果赋值了,那么可以省略类型。具体被赋值成啥,看精度。如int
,float64
,complex128
等等
var c, python, java = true, false, "no!"
在函数中,简洁赋值语句 :=
可在类型明确的地方代替 var
声明。
:=
结构不能在函数外使用,只能用var
基本类型
bool
string
int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名 // 表示一个 Unicode 码点
float32 float64
complex64 complex128
变量声明也可以“分组”成一个语法块。
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
没有明确初始值的变量声明会被赋予它们的 零值。
0,false,“”
类型转换
i := 42 // int
f := float64(i)
u := uint(f)
流程控制
for
Go 只有一种循环结构:for
循环。
基本的 for
循环由三部分组成,它们用分号隔开:
初始化语句:在第一次迭代前执行。仅在for作用域中可见。
条件表达式:在每次迭代前求值
后置语句:在每次迭代的结尾执行
可以省略分号变成while,甚至省略条件变成死循环。
if
不需要小括号,可以在前面加一个分号来执行简单语句。作用域就在if
的范围内。
if v := math.Pow(x, n); v < lim {
return v
}
switch
相当于一大串的if-else
。只运行选定的case
,因为自动提供了break
。
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。
不加条件的switch
能看起来更清楚。
defer
defer 语句会将函数推迟到外层函数返回之后执行。【函数结束前,执行这个defer内容】
func main() {
defer fmt.Println("world") // hello 返回后才会执行 world
fmt.Println("hello")
}
defer很多,就会压入堆栈中。后进先出
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i) // 0-9压入,9-0推出
}
fmt.Println("done")
}
更多类型
指针
指针保存了值的内存地址。go没有指针运算。
&
操作符会生成一个指向其操作数的指针。&i
获得i
的内存地址,如0xc000090000*
操作符表示通过指针指向变量的值。*
后面要加上地址,这能获取这个地址的值,如*(&i)=i
,都是i的值。
i := 42, 2701
p := &i // 指向 i,此时p里面保存的是一个内存地址,如0xc000090000
// 当然p也是变量,也有存p的地址。如&p=0xc00000e010
fmt.Println(*p) // 通过指针p读取 i 的值
*p = 21 // 通过指针p设置 i 的值 【相当于i=21】
fmt.Println(i) // 查看 i 的值【相当于*p】
p := &i
中的p
是i
的地址,*p
是顺着地址找到了i
的值
结构体
一个结构体(struct
)就是一组字段(field)。
【感觉是数组的升级版,数组只能全部是一个类型,而结构体可以不同东西有不同类型】
type Vertex struct {
X int
Y int
}
// 写成这样也可以
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
// 相当于
// v1 = Vertex{X: 1, Y: 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0,取int的零值
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {
fmt.Println(Vertex{1, 2}) // {1 2}
v := Vertex{1, 2}
v.X = 4 // 用点来访问
fmt.Println(v.X)
}
数组
类型 [n]T
表示拥有 n
个 T
类型的值的数组。
var a [10]int
,表示将变量 a
声明为拥有 10 个整数的数组。
数组不能改变大小。长度是其类型的一部分
【感觉channel就是数组做出来的】
切片
引用一部分数组。它并不存储任何数据,它只是引用了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。【直接通过指针改变了对应的值】
类型 []T
表示一个元素类型为 T
的切片。
a[1:4]
包含数组 a
中下标从 1 到 3 的元素。和python
一样,不写就默认是上下界。
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
// 也可以让编译器自己去统计
names := [...]string{ // ...代替数字即可
"John",
"Paul",
"George",
"Ringo",
}
a := names[0:2]
b := names[1:3]
fmt.Println(a, b) // [John Paul] [Paul George]
b[0] = "XXX"
fmt.Println(a, b) // [John XXX] [XXX George]
fmt.Println(names)
// a[:] == a[0:4] == a[:4] == a[0:]
}
除了用[1:4]
来切片,还可以用无长度的数组来创建
// 创建数组
a := [6]int{2, 3, 5, 7, 11, 13}
// 创建上面一样的数组,然后用切片来引用
q := []int{2, 3, 5, 7, 11, 13}
// 相当于 q:= a[:]
s := []struct { // 类型可以是结构体
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
}
fmt.Println(s) // [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]
切片的长度和容量
切片s
,长度是len(s)
,容量是cap(s)
。
左指针改容量,右指针不改容量。因为切片的index
必须是一个非负数,所以取不到-1
,所以左指针就规定了容量。
容量,也就是可能取到的长度。
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s) // len=6 cap=6 [2 3 5 7 11 13]
// 截取切片使其长度为 0
s = s[:0]
printSlice(s) // len=0 cap=6 []
// 拓展其长度
s = s[:4]
printSlice(s) // len=4 cap=6 [2 3 5 7]
// 舍弃前两个值
s = s[2:]
printSlice(s) // len=2 cap=4 [5 7]
s = s[:0]
printSlice(s) // len=0 cap=4 []
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
长度是切片的内容长度,容量是从左边指针的切片开始算起的。

切片的零值
切片的零值是 nil
。nil 切片的长度和容量为 0 且没有底层数组。
make创建切片
a := make([]int, 5) // 感觉只是用来创建全0的切片
// s == []int{0, 0, 0, 0, 0}
printSlice("a", a) // a len=5 cap=5 [0 0 0 0 0]
b := make([]int, 0, 5) // 长度0,容量5
printSlice("b", b) // b len=0 cap=5 []
c := b[:2] // 长度变成2
printSlice("c", c) // c len=2 cap=5 [0 0]
d := c[2:5] // 容量-2,变成了3,长度是3
printSlice("d", d) // d len=3 cap=3 [0 0 0]
i := b[:cap(b)] // 可以直接算出b的最大容量
printSlice("i", i) // i len=5 cap=5 [0 0 0 0 0]
k := b[:] // 这样会继承b的长度,也就是0
printSlice("k", k) // k len=0 cap=5 []
e := make([]int, 0, 3)
f := e[:5]
printSlice("f", f) // panic: runtime error: slice bounds out of range
// 容量只有3,取不到5
切片追加元素append
func append(slice []Type, elems ...Type) []Type
append 内建函数将元素追加到切片的末尾。 若它有足够的容量,其目标就会重新切片以容纳新的元素。否则,就会分配一个新的基本数组。 append 返回更新后的切片。因此必须存储追加后的结果,通常为包含该切片自身的变量:
slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...)
作为一种特殊的情况,将字符追加到字节数组之后是合法的,就像这样:
slice = append([]byte("hello "), "world"...)
var s []int
printSlice(s) // len=0 cap=0 []
// 添加一个空切片
s = append(s, 0) // 原数组容量为0,不够添加,因此新建一个更大的数组分配给这个切片
printSlice(s) // len=1 cap=2 [0] // 新数组的容量为2,是添加的一个数的两倍
// 这个切片会按需增长
s = append(s, 1) // 容量够,就直接加。【估计是把原数组的[0 0]变成了[0 1]】
printSlice(s) // len=2 cap=2 [0 1]
// 可以一次性添加多个元素
s = append(s, 2, 3, 4) // 添加了3个,容量不够再扩容。
printSlice(s) // len=5 cap=8 [0 1 2 3 4]
// 新数组变成了8,因为增加了3个,翻倍要6个位置,原来有俩,6+2=8
Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。
当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。【js当中也是这样,重新赋值一个值,如string,相当于照样新建了一个值。与原来的毫无关系。js参考】
(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。)
可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定。
切片的内幕一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
make([]byte, 5)
相当于
s = s[2:4]
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。【改变头的指针地址】
通过一个新切片修改元素会影响到原始切片的对应元素。
切片的生长要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。
可能的陷阱切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。
这样的话,有时候可能会因为一个小的内存引用导致保存所有的数据。
【简言之要用切片来操作,而非操作整个数组。感觉切片的出现就是为了避免整个数组的操作】
range
for
循环的 range
形式可遍历切片或映射。
每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
可以用_
来忽略
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
// 忽略
for _, value := range pow
for i, _ := range pow
// 忽略第二个,可以忽略_
for i := range pow
}
练习:切片
实现
Pic
。它应当返回一个长度为dy
的切片,其中每个元素是一个长度为dx
,元素类型为uint8
的切片。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。图像的选择由你来定。几个有趣的函数包括
(x+y)/2
,x*y
,x^y
,x*log(y)
和x%(y+1)
。(提示:需要使用循环来分配
[][]uint8
中的每个[]uint8
;请使用uint8(intValue)
在类型之间转换;你可能会用到math
包中的函数。)作者:BigManing 来源:CSDN 原文:https://blog.csdn.net/qq_27818541/article/details/54346106
package main
import "golang.org/x/tour/pic"
func Pic(dx, dy int) [][]uint8 {
result := make([][]uint8, dy)
for x := range result {
item := make([]uint8, dx)
for y := range item {
item[y] = uint8((x + y) / 2)
}
result[x] = item
}
return result
}
func main() {
pic.Show(Pic)
}

感觉x^y
最好看

映射map
将键映射到值。零值为nil,没有键也不能添加键。
type Vertex struct { // 设定Vertex结构体
Lat, Long float64
}
func main() {
m := make(map[string]Vertex) // 新建一个map,key是string,value是Vertex
m["Bell Labs"] = Vertex{ // 给map 的key赋值
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"]) // {40.68433 -74.39967}
}
// 或者也可以直接定义:
var m = map[string]Vertex{ // [string]Vertex是为了表明key和value类型吧
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
// 也可以这么定义
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
// 删除
delete(m, key)
// 检验值是否存在
elem, ok := m[key]
// 若 key 在 m 中,ok 为 true ;否则,ok 为 false, elem也是零值
练习:映射
实现
WordCount
。它应当返回一个映射,其中包含字符串s
中每个“单词”的个数。函数wc.Test
会对此函数执行一系列测试用例,并输出成功还是失败。你会发现 strings.Fields 很有帮助。
package main
import (
// "fmt"
"runtime"
"strings"
)
import (
"golang.org/x/tour/wc"
)
func WordCount(s string) map[string]int {
str := strings.Fields(s)
m := make(map[string]int)
for _, elem := range str {
if _, ok := m[elem]; ok {
m[elem] += 1
} else {
m[elem] = 1
}
runtime.Gosched()
}
return m
}
func main() {
result := WordCount("I am learning Go!")
fmt.Println(result)
wc.Test(WordCount)
}
函数
函数也是值,可以像值一样被传递。
闭包
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
练习:斐波纳契闭包
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
first := 0
second := 1
return func() int {
result := first
first, second = second, (first + second) // 这种赋值方式可以保留second的值
return result
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法和接口
方法
就是给结构体加一个方法。
type Vertex struct { // 结构体
X, Y float64
}
func (v Vertex) Abs() float64 { // 在结构体下面,加一个Abs()方法,返回float64
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) // 调用结构体的方法,结果是5
}
// 如果是正常的函数,是这么写的
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
}
// 非结构体,也可以有方法
// 感觉就是在某种type下面增加一种方法。
// 但类型定义和方法声明必须在同一包内。且不能为内建类型声明方法。
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
这些都是在值下面增加方法。因此处理的都是输入的值的副本
如果用指针,就是在直接操作值本身。
func (v Vertex) Scale(f float64) { // 这是在操作值的副本
v.X = v.X * f
v.Y = v.Y * f
}
// 函数结束后,副本就会被销毁,v的两个值都不会变
func (v *Vertex) Scale(f float64) { // 这是在操作指针
v.X = v.X * f
v.Y = v.Y * f
}
// 直接修改了指向的内容,函数结束后v的两个值都乘f了
func main() {
v := Vertex{3, 4}
v.Scale(10) // 使用的时候不需要加&
fmt.Println(v.Abs())
}
// 用函数写:
type Vertex struct {
X, Y float64
}
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Scale(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
Scale(&v, 10) // 传进去的是指针,要加一个&
fmt.Println(Abs(v))
}
函数如果需要传入指针(输入有*),那么必须传入(参数&,不然报错)
而type下面的方法就不需要,解释器会默认处理好。
一般都是传地址,复制值成本太高
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4} // 不传地址也能跑对,建议穿地址
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
接口
// 定义两个类型
type MyFloat float64
type Vertex struct {
X, Y float64
}
f := MyFloat(-math.Sqrt2) // f是MyFloat的一个实例
v := Vertex{3, 4}
b := f
b = v // 这样会报错,因为b在f的时候,类型已经定义为MyFloat了,不能重复定义
// 定义一个空接口
type Abser interface {} // 一定要有个括号
var a Abser // 定义a为接口类型,就什么东西都能赋值了,类似于any
// 这可以用来处理未知类型的值
a = f
a = v
a = &v
// 如果a想要用某个方法,可以在下面定义
type Abser interface {
Abs() float64
}
// 不过需要赋值的类型,下面得有这个方法,不然会报错
func (f MyFloat) Abs() float64 { // myfloat下面增加一个abs()方法
...
}
func (v *Vertex) Abs() float64 { // vertex下面增加一个abs()方法,需要传入地址
...
}
a = f
a = v
a = &v
a.Abs()
// 能赋值也能调用方法
// 如果展开赋值就是这样:
var b Abser = Vertex{3, 4}
// 没办法用:=定义类型
// b := Vertex{3, 4} // 这就变成Vertex类型了,而不是Abser
一个防御输入值为nil
的方法:
type T struct {
S string
}
func (t *T) M() { // 增加一个函数
if t == nil { // 如果是nil
fmt.Println("<nil>") // 就做处理
return
}
fmt.Println(t.S)
}
如果值是nil
,那么调用其底下方法,就会产生panic
报错。
因为不知道该用哪个类型的方法。【可能因为nil
下面并没有这个方法】
func main() {
var i I // nil
i.M() // panic
}
类型断言
语法:t := i.(T)
,断言i
为T
类型。
var i interface{} = "hello" // i是任意类型,此处赋值为string
s := i.(string) // 断言为 string
fmt.Println(s)
f = i.(float64) // 断言为float64
fmt.Println(f) // 断言错,就会报错(panic)
// 避免断言错的报错,就要收第二个参数,即断言是否正确
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
类型选择
从几个类型断言中选择分支的结构。
就是一种根据不同输入类型来选择操作的switch
结构。类型用type
表示
switch v := i.(type) { // 括号里是 type
case int: // 如果输入类型是int
fmt.Printf("Twice %v is %v\n", v, v*2)
case string: // 如果输入类型是string
fmt.Printf("%q is %v bytes long\n", v, len(v))
default: // 默认情况,v和i的接口类型和值相同
fmt.Printf("I don't know about type %T!\n", v)
}
stringer
fmt
默认的打印方法,是调用结构体下面的String()
方法。
因此可以通过修改String()
的方法,来修改该结构体的打印值。
【感觉这种能操纵默认方法的操作,非常有毒】
type Person struct {
Name string
Age int
}
func main() {
a := Person{"123", 3}
b := Person{"234", 5}
fmt.Println(a, b) // {123 3} {234 5}
}
// 修改默认的String()方法
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
// 就会根据这个String()来打印,因为fmt默认调用的是这个方法
...
fmt.Println(a, b) // 123 (3 years) 234 (5 years)
...
// 如果把String()改成;
func (p Person) String() string {
return fmt.Sprintln("abc")
}
// 结果会打印
// abc
// 其实如果想改变该结构体的输出方法,完全可以通过加一个函数,而不是直接改变默认函数
func (p Person) PrintABC() string {
return fmt.Sprintln("abc")
}
...
fmt.Println("z.PrintABC()", z.PrintABC())
...
练习:Stringer
输入IPAddr{1, 2, 3, 4}
打印为 "1.2.3.4"
。
go似乎不能直接对数组进行操作,这是一个不可变的东西。而又查到了,可以对string
进行split
和join
。
因此一开始的想法,是把数组一个个拿出来转成string
,然后再进行split
和join
成想要的地址样子。
不过byte
转string
,能查到的方法,只有在前面加一个string()
,如string(p)
。但这个仅对[]byte类型有用,而这里的p
的类型是IPAddr
,所以就不能用。
查到了这个方法来解决:
作者:BigManing 来源:CSDN 原文:https://blog.csdn.net/qq_27818541/article/details/54347335
package main
import (
"fmt"
"strconv"
)
func (p IPAddr) String() string {
lastIndex := len(p) - 1
var s string
for i, v := range p {
s += strconv.Itoa(int(v)) // uint8 > int > string
if i != lastIndex { // 加点
s += "."
}
}
return s
}
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
感觉加点可以不用那么麻烦,最后删掉一位字符串即可
func (p IPAddr) String() string {
var s string
for _, v := range p {
s += strconv.Itoa(int(v)) + "." // 每个后面都加一个点
}
return s[:len(s)-1] // 最后少返回一位
}
不过go里面的数组都是值,是不可变的,因此字符串拼接的操作,其实是创建了一个新的字符串,其值是两个字符串拼接的而已。包括最后的切一位,也是生成了一个新的字符串。
也就是说,循环了len(p)
次数,意味着产生了len(p)
个字符串,不过之前的都被废弃了,只剩下最后一个而已。换言之这对内存成本很高。
然后发现了一个绕过拼接字符串的方法。
func (p IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", p[0], p[1], p[2], p[3])
}
主要是Sprintf
这个函数的操作。参考
错误
error
类型是一个内建接口
i, err := strconv.Atoi("42")
if err != nil { // 错误处理,如果有错就运行
fmt.Printf("couldn't convert number: %v\n", err)
return
}
error
为 nil 时表示成功;非 nil 的 error
表示失败。
练习:错误
package main
import (
"fmt"
"math"
)
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprint("cannot sqrt negative number: ", float64(e))
}
func mySqrt(x float64) (float64, error) {
if x < 0 {
return 0.0, ErrNegativeSqrt(x) // 把x从float64转成ErrNegativeSqrt
}
z := float64(1.0)
for delta := float64(100); delta > 0.000001; {
pre := z
z -= (z*z - x) / (2 * z)
delta = math.Abs(z - pre)
}
return z, nil
}
func main() {
fmt.Println(mySqrt(2))
fmt.Println(mySqrt(-2))
}
ErrNegativeSqrt(x)
是把x
从float64
转成ErrNegativeSqrt
。
作为error
输出的时候,会优先调用Error()
来打印。然而这个函数返回的也是一个打印,因此如果用e
本身,就会触发死循环。得转换一下格式才行。
reader
一次读取多少字节
func main() {
r := strings.NewReader("Hello, Reader!") // 必须要这么定义,只是string就读取不了
b := make([]byte, 8) // 一次读取8字节
for {
n, err := r.Read(b) // 从r中读取b个大小的东西
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
// n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
fmt.Printf("b[:n] = %q\n", b[:n])
// b[:n] = "Hello, R",读取了这么多
if err == io.EOF { // 如果读光了就报错 err = EOF
break
}
}
}
练习:reader
package main
import "golang.org/x/tour/reader"
type MyReader struct{}
func (m MyReader) Read(b []byte) (int, error) {
b[0] = 'A'
return 1, nil
}
func main() {
reader.Validate(MyReader{})
}
不是很懂这个😂😂
练习:rot13Reader
就是用reader解密咯。加密方法是ROT13。用一遍加密,再用一遍解密。因为就是把字母替换为其13位后面的字母。
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func rot13(b byte) byte {
switch {
case 'A' <= b && b <= 'M':
b = b + 13
case 'M' < b && b <= 'Z':
b = b - 13
case 'a' <= b && b <= 'm':
b = b + 13
case 'm' < b && b <= 'z':
b = b - 13
}
return b
}
func (mr rot13Reader) Read(b []byte) (int, error) {
n, e := mr.r.Read(b)
for i := 0; i < n; i++ {
b[i] = rot13(b[i])
}
return n, e
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
图像
image包
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
练习:图像
package main
import (
"image"
"image/color"
"golang.org/x/tour/pic"
)
type Image struct{}
func (i Image) ColorModel() color.Model {
return color.RGBAModel
}
func (i Image) Bounds() image.Rectangle {
return image.Rect(0, 0, 200, 200)
}
func (i Image) At(x, y int) color.Color {
return color.RGBA{R: uint8(x), G: uint8(y), B: uint8(255), A: uint8(255)}
}
func main() {
m := Image{}
pic.ShowImage(m)
}

并发
go routine
goroutine是由 Go 运行时管理的轻量级线程。
go f(x, y, z)
会启动一个新的 Go 程并执行
f(x, y, z)
在新的 Go 程中只负责执行 f
,其他的求值发生在当前的 Go 程中。
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。
信道
信道是带有类型的管道,你可以通过它用信道操作符 <-
来发送或者接收值。
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将和送入 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收
fmt.Println(x, y, x+y)
}
如果计算量小的话,就会在第二个协程开启前,就已经把第一个协程内容删掉了
带缓冲的信道
make初始化信道,第二个参数是缓冲长度
ch := make(chan int, 100)
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。
【满了不让塞,空了不让拿】
range 和 close
发送者可通过 close
关闭一个信道来表示没有需要发送的值了。
接收者可以通过为接收的第二个参数,来判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
v, ok := <-ch
之后 ok
会被设置为 false
。
循环 for i := range c
会不断从信道接收值,直到它被关闭。
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range
循环。
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
_,ok:=<-c
fmt.Println(ok)
}
select
select
语句使一个 Go 程可以等待多个通信操作。
select
会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
可以用struct{}改写,这样省内存空间。
func fibonacci(c chan int, quit chan struct{}) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan struct{})
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- struct{}{}
// 第一个括号,是struct的类型,是空;第二个是其内容,是空
}()
fibonacci(c, quit)
}
默认选择
为了在尝试发送或者接收时不发生阻塞,可使用 default
分支:
select {
case i := <-c:
// 使用 i
default:
// 从 c 中接收会阻塞时执行
}
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
练习:等价二叉查找树
树的结构是这样的:
type Tree struct {
Left *Tree
Value int
Right *Tree
}
要写一个walk函数,来一个个把树的内容传到信道里面,然后用same函数来比较两个树是否一样。
// 原文链接:https://blog.csdn.net/qq_27818541/article/details/54411990
package main
import (
"fmt"
"golang.org/x/tour/tree"
)
// 递归向channel传值
func sendValue(t *tree.Tree, ch chan int) {
if t != nil {
sendValue(t.Left, ch)
ch <- t.Value
sendValue(t.Right, ch)
}
}
// 发送value,结束后关闭channel
func Walk(t *tree.Tree, ch chan int) {
sendValue(t, ch)
close(ch)
}
// 使用写好的Walk函数来确定两个tree对象 是否一样 原理还是判断value值
func Same(t1, t2 *tree.Tree) bool {
ch1 := make(chan int)
ch2 := make(chan int)
go Walk(t1, ch1)
go Walk(t2, ch2)
for i := range ch1 { // ch1 关闭后 for循环自动跳出
if i != <-ch2 {
return false
}
}
return true
}
func main() {
// 打印 tree.New(1)的值
var ch = make(chan int)
go Walk(tree.New(1), ch)
for v := range ch {
fmt.Println(v)
}
// 比较两个tree的value值是否相等
fmt.Println(Same(tree.New(1), tree.New(1)))
fmt.Println(Same(tree.New(1), tree.New(2)))
}
sync.Mutex
若想避免冲突,保证每次只有一个 Go 程能够访问一个共享的变量,从而
这里涉及的概念叫做 互斥(mutualexclusion) ,我们通常使用 互斥锁(Mutex)* 这一数据结构来提供这种机制。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:
Lock
Unlock
我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。 我们也可以用 defer 语句来保证互斥锁一定会被解锁
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex // 互斥锁
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
web爬虫
Last updated
Was this helpful?