目前打算的学习路线:
GO语言之旅
GO官方文档
有标注的例子们
书籍:The GO Programming Language
书籍:Go in action
基础
包、变量和函数
包:按照约定,包名与导入路径的最后一个元素一致。例如,“math/rand” 包中的源码均以 package rand
语句开始。
导入
导出名:在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。
函数:类型在变量名之后,同类型可省略func add(x, y int) int { return x + y }
多值返回:func swap(x, y string) (string, string) { return y, x }
命名返回值:没有参数的 return 语句返回已命名的返回值。也就是 直接 返回。
变量:var 语句可以出现在包或函数级别。
变量初始化:var c, python, java = true, false, "no!"
短变量声明:函数外的每个语句都必须以关键字开始(var, func
等等),因此 :=
结构不能在函数外使用。
基本类型:int, uint
和 uintptr
在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。
数值常量:数值常量是高精度的 值。
一个未指定类型的常量由上下文来决定其类型。
1<<100:MAX
,再左移会溢出
流程控制语句:for、if、else、switch和defer
循环只有for
for i := 0; i < 10; i++ {
sum += i
}
for可以没有初始条件和后置语句,此时for就是while
for sum<1000{sum+=sum}
if :同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。
该语句声明的变量作用域仅在 if 之内。
switch:Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。
没有条件的 switch 同 switch true 一样。
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
defer :会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
关闭文件流、关锁、printFooter
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
三个规则:
- A deferred function’s arguments are evaluated when the defer statement is evaluated. 立即求值
- Deferred function calls are executed in Last In First Out order after the surrounding function returns. 先进后出
- Deferred functions may read and assign to the returning function’s named return values. defer的函数可以修改返回值。作用:修饰错误结果
func c() (i int) {
defer func() { i++ }()
return 1
}
Defer相关:Panic和Recover
Panic:When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller.
Recover:If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
输出
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
更多类型:Struct、slice 和 映射
指针:与 C 不同,Go 指针没有运算。GO函数都是传值,map则是GO通过包装省去了指针操作,依然是传值。map===*hmap
、chan=*hchan
,slice也是模仿引用类型。
结构体是一组字段
type Vertex struct {
X int
Y int
}
fmt.Println(Vertex{1,2}) //{1 2}
v:=Vertex{1,2}
v.X=2
p:=&v
p.X=1e9 //p.X就是(*p).X
数组:数组的长度是其类型的一部分,因此数组不能改变大小。
var a [2]string
切片:切片为数组元素提供动态大小的、灵活的视角。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改。
对比数组文法和切片文法:
数组:
[3]bool{true, true, false}
切片:创建一个相同的数组然后构建一个它的切片
[]bool{true, true, false}
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
nil切片:切片的零值是 nil。
nil 切片的长度和容量为 0 且没有底层数组。
make创建切片:产生一个元素为0值的数组并返回它的切片
b := make([]int, 0, 5) //类型、长度、容量
切片的切片:切片可以包含任何类型,甚至是它的切片。
向切片追加元素:s=append(s,2,3)
GO切片:用法和本质
Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。(理解:指向数组的指针指向这一整个数组,数组是一个巨大的结构,其中的元素是其成员)
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。
a = append(a, b...)
切片展开作为元素
Range:用于遍历切片或映射。当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
只需要索引:for i := range pow
map:映射
m:=make(map[string]Vertex) //初始化
m[key] = elem //赋值
elem = m[key] //获取
elem, ok := m[key] //获取,检验有无
函数值
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4) //参数是一个func,执行func(3,4),返回float64
}
闭包:闭包函数会引用函数体以外的值,可以对其修改。闭包可以重复使用局部变量同时不污染全局。
- 外层函数嵌套内层函数
- 内层函数使用外层函数的局部变量
- 把内层函数作为外层函数的返回值
func adder() func(int) int { //返回值是一个函数:函数接收int,返回int
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
package main
import "fmt"
func fibonacci() func() int {
x1 := 0
x2 := 1
return func() int {
defer func() {
x1 += x2
x1, x2 = x2, x1
}()
return x1
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法与接口
方法:Go没有类。不过可以为结构体类型定义方法。方法就是一类带特殊的 接收者 参数的函数。
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。
接收者的类型可以用 *T 的文法。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
方法与指针重定向:带指针参数的函数必须接受一个指针;而以指针为接收者的方法被调用时,接收者既能为值又能为指针;Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。
为什么使用指针接收者:
- 方法能够修改其接收者指向的值
- 以避免在每次调用方法时复制该值
接口:一组方法签名定义的集合。
接口的隐式实现:
type I interface {
M()
}
type T struct {
S string
}
// 此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello"}
i.M()
}
底层值为 nil 的接口值:优雅地处理它有木有
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
nil接口值:
为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
空接口:空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
类型断言:提供了访问接口值底层具体值的方式。
t, ok := i.(T)
类型选择
按顺序从几个类型断言中选择分支的结构
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
/*Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!*/
Stringer
type Stringer interface {
String() string
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func (ip IPAddr) String() string {
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}
Error
package main
import (
"fmt"
"math"
)
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v\n",float64(e))
}
func Sqrt(x float64) (float64, error) {
if x >= 0 {
return math.Sqrt(x), nil
} else {
return 0, ErrNegativeSqrt(x)
}
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
io.Reader
func (T) Read(b []byte) (n int, err error)
//Read 用数据填充给定的字节切片并返回填充的字节数和错误值。
例子:
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func rot13(out byte) byte { //字母转换
switch {
case out >= 'A' && out <= 'M' || out >= 'a' && out <= 'm':
out += 13
case out >= 'N' && out <= 'Z' || out >= 'n' && out <= 'z':
out -= 13
}
return out
}
func (f rot13Reader) Read(b []byte) (int, error) {
n, err := f.r.Read(b)
for i:=0;i<n;i++{
b[i]=rot13(b[i])
}
return n, err
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
并发
Goroutine: Go 运行时管理的轻量级线程。
信道:带有类型的管道,可以通过它用信道操作符 <- 来发送或者接收值。
ch := make(chan int) //创建信道。
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
带缓冲信道:ch := make(chan int, 100)
信道的关闭
发送者关闭:close(c)
接收者测试:v, ok := <-ch
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
select 语句:使一个 Go 程可以等待多个通信操作
package main
import "fmt"
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)
}
select 可以加入default
练习:等价查找二叉树(居然自己写出来了,太感人了)
package main
import "golang.org/x/tour/tree"
import "fmt"
// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int){
for i:=range(t.String()){
ch<-i
}
close(ch)
}
// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool{
ch1:=make(chan int)
ch2:=make(chan int)
go Walk(t1,ch1)
go Walk(t2,ch2)
var v2 int
var ok2 bool
for v1,ok:=<-ch1;ok==true;v1,ok=<-ch1{
v2,ok2=<-ch2
if ok2==false || v1!=v2{
return false
}
}
v2,ok2=<-ch2
if ok2==true{
return false
}
return true
}
func main() {
if Same(tree.New(1), tree.New(2)){
fmt.Println("Same.")
}else {
fmt.Println("Different.")
}
}
最后两个练习 :sync.Mutex、Web爬虫,注意到多线程要等待结束要使用sync.Waitgroup,它确保所有线程完成才退出
关于waitgroup和官方用的channel的比较点这里注意这里标答的channel是不符合GO语言风格的:
Channel
passing ownership of data,
distributing units of work,
communicating async results
Mutex
caches,
state