Free Talk
因为下个月将要去字节实习,技术栈要从 Java 转为 Goland ,大约半年多前,我粗略地学习过一次 Goland ,完成了一个简单项目研发。这次打算系统地学习一下 Goland ,这篇文章主要是阅读 The Little Go Book 的笔记,适合有一定其他语言经验的开发者快速了解 Goland。
基础知识
简单定义
Go是一种编译型 、具有静态类型 和类C语言语法的语言,并具备垃圾回收机制 。
编译型之前已经讲过, 不再赘述。
**静态类型:**变量必须指定一个类型,可以在声明变量时指定变量类型,但是在大多数情况下,让编译器去自动推断类型。
下载地址:https://golang.org/dl/
程序示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package main import ( "fmt" "os" ) func main () { if len (os.Args) != 2 { os.Exit(1 ) } fmt.Println("It's over " , os.Args[1 ]) }
变量和声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" ) func main () { var power int power = 9000 fmt.Printf("It's over %d\n" , power) }
函数声明
Go语言函数支持多值返回
1 2 3 4 5 6 7 8 9 10 11 func log (message string ) {} func add (a int , b int ) int {} func power (name string ) (int , bool ) {}
1 2 3 4 5 6 _, exists := power("goku" ) if exists == false { }
结构体
Go不是面向对象语言,没有对象和继承的概念,因此也不存在多态和重载等特性。
1 2 3 4 type Saiyan struct { Name string Power int }
声明和初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 goku := Saiyan{ Name: "Goku" , Power: 9000 , } goku := Saiyan{} goku := Saiyan{Name: "Goku" } goku.Power = 9000 goku := Saiyan{"Goku" , 9000 }
指针
Go不希望我一个变量直接关联一个值,而是希望一个指针指向变量的值,即间接取值 。
Go中,函数的参数传递都是按值传递,即传递的是一个拷贝。
1 2 3 4 5 6 7 8 9 10 11 func main () { goku := Saiyan{"Goku" , 9000 } Super(goku) fmt.Println(goku.Power) } func Super (s Saiyan) { s.Power += 10000 }
1 2 3 4 5 6 7 8 9 10 11 func main () { goku := &Saiyan{"Goku" , 9000 } Super(goku) fmt.Println(goku.Power) } func Super (s *Saiyan) { s.Power += 10000 }
赋值一个指针变量的开销比复制一个复杂的结构体小的多,在一个64位的系统上,指针的大小只有64位(就是表示一个内存地址),因此指针的真正意义就是通过指针可以共享值 。
结构体上的函数
1 2 3 4 5 6 7 8 9 type Saiyan struct { Name string Power int } func (s *Saiyan) Super () { s.Power += 10000 }
1 2 3 4 goku := &Saiyan{"Goku" , 9001 } goku.Super() fmt.Println(goku.Power)
构造函数
结构体没有构造函数,可以创建一个函数返回一个相应类型的实例代替(类似一个工厂)
1 2 3 4 5 6 func NewSaiyan (name string , power int ) Saiyan { return Saiyan{ Name: name, Power: power, } }
new
尽管没有构造函数,Go有一个内置的函数new,用来分配一个类型需要的内存
1 2 3 goku := new (Saiyan) goku := &Saiyan{}
结构体字段
结构体里面还可以嵌套其他结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Saiyan struct { Name string Power int Father *Saiyan } gohan := &Saiyan{ Name: "Gohan" , Power: 1000 , Father: &Saiyan { Name: "Goku" , Power: 9001 , Father: nil , }, }
组合
Go 使用组合来替代继承,这本书这部分讲的不清楚,我打算重新梳理一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Animal struct { Name string } func (a *Animal) Eat () { fmt.Printf("%v is eating" , a.Name) fmt.Println() } type Cat struct { *Animal } cat := &Cat{ Animal: &Animal{ Name: "cat" , }, } cat.Eat()
Cat 的结构体本身没有 Name 字段,也没有去实现 Eat 方法,但却得到了正常的输出。
这边我看了一些文章,还是了解的比较浅,之后再深入了解。
指针类型和值类型
在以下情况我们都有实现指针类型,因为传递值类型的数据不可变:
一个局部变量赋值
结构体字段
函数返回值
传递给函数的参数
方法的接收者
如果不确定使用哪个,就使用指针。
映射、数组和切片
数组
Go 中数组是固定大小 的,声明一个数组时我们必须指明它的大小,并且不能被扩展变大。
1 2 var scores [10 ]int scores[0 ] = 339
切片
切片是一个轻量级的结构体,代表数组的一部分。
1 2 3 4 5 6 7 8 9 10 scores := []int {1 ,4 ,293 ,4 ,9 } scores := make ([]int , 10 ) scores := make ([]int ,0 ,10 )
使用 append 函数可以扩展切片长度,如果底层的数组已经达到上限, append 会重新创建一个更大的数组,使用2倍算法。
1 2 3 4 5 func main () { scores := make ([]int , 0 , 10 ) scores = append (scores, 5 ) fmt.Println(scores) }
映射
Go 中的映射和 Java 的中哈希表其实一样,包含一个键和值,可以从映射中获取、设置和删除这个值。
和切片一样,映射也是可以通过 make
创建的:
1 2 3 4 5 6 7 8 func main () { lookup := make (map [string ]int ) lookup["goku" ] = 9001 power, exists := lookup["vegeta" ] fmt.Println(power, exists) }
使用len
可以获得映射中键的个数。使用delete
可以删除映射中的一个键值对。
1 2 3 4 total := len (lookup) delete (lookup, "goku" )
映射是动态增长的,可以在使用 make
时传递第二个参数设置映射的初始大小:
1 lookup := make (map [string ]int , 100 )
代码组织和接口
包
在 Go 中,包名和你的 Go 语言工作空间的目录结构有关。
默认路径为:$GOPATH/src
循环导入
简单讲就是 A 包导入 B 包,B 包导入 A 包。
可见性
如果命令类型或者函数时一一个大写字母开发,那么这个类型和函数就是可见的。
包管理
go get 可以将远程文件保存到工作空间 $GOPATH/src
接口
Go 的接口和 Java 很像,是一种类型,只定义了声明,没有具体实现,用于代码解耦。
1 2 3 type Logger interface { Log(message string ) }
Go特性
错误处理
Go 语言没有异常处理,一般通过返回值处理错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "os" "strconv" ) func main () { if len (os.Args) != 2 { os.Exit(1 ) } n, err := strconv.Atoi(os.Args[1 ]) if err != nil { fmt.Println("not a valid number" ) } else { fmt.Println(n) } }
defer
Go 语言提供了垃圾回收机制,但是一些资源需要手动释放更安全,使用 defer 关键词释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" "os" ) func main () { file, err := os.Open("a_file_to_read" ) if err != nil { fmt.Println(err) return } defer file.Close() }
Go语言风格
Go 语言程序都遵循相同的格式化规则,如使用 tab 缩进并且花括号和语句在同一行。
当你在工程内部,你可以通过下面的命令将工程下所有文件使用相同的格式化规则:
初始化的if
一个值可以在条件语句执行前定义并初始化:
1 2 3 if x := 10 ; count > x { ... }
if
语句中定义并初始化的值在if
语句之外是不可用的,但是可以在else if
和else
语句中使用。
空接口和转换
在大多数面向对象语言中,都有一种内置的基类,叫object
,它是所有其他类的超类。但是go语言不支持继承,所以没有类似超类的概念。go拥有一个没有任何方法的空接口:interface{}
。因为每种类型都实现了空接口的0个方法,并且接口都是隐式实现,所以每种类型都实现了空接口的条约。
字符串和字节数组
字符串和字节数组有密切关系,我们可以轻易的将它们转换成对方:
1 2 3 stra := "the spice must flow" byts := []byte (stra) strb := string (byts)
并发
Go 协程
Go 协程类似一个线程,但是 Go 协程是有 Go 自己调度,而不是系统。在协程中的代码可以和其他代码并发执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "time" ) func main () { fmt.Println("start" ) go process() time.Sleep(time.Millisecond * 10 ) fmt.Println("done" ) } func process () { fmt.Println("processing" ) }
Go 协程很容易创建且开销较小。最终多个 Go 协程将会在同一个底层的系统线程上运行。这也常称之为**M:N
线程模型**,因为我们有M
个应用线程(Go 协程)运行在N
个系统线程上。结果就是,一个 Go 协程的开销和系统线程比起来相对很低(一般都是几KB)。在现代的硬件上,有可能拥有成千上万个 Go 协程。
同步
在编写并发执行的代码时,特别需要关注的是在哪里 和如何 读写一个值(Where、How)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "time" ) var counter = 0 func main () { for i := 0 ; i < 2 ; i++ { go incr() } time.Sleep(time.Millisecond * 10 ) } func incr () { counter++ fmt.Println(counter) }
这里应该有两个协程同时读写 counter 变量,可以使用一个互斥锁保证原子性。
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 package mainimport ( "fmt" "sync" "time" ) var ( counter = 0 lock sync.Mutex ) func main () { for i := 0 ; i < 2 ; i++ { go incr() } time.Sleep(time.Millisecond * 10 ) } func incr () { lock.Lock() defer lock.Unlock() counter++ fmt.Println(counter) }
关于 Go 并发编程这块内容很深,涉及死锁,读写锁等等其他问题。
通道
并发编程最难的就是共享数据,在Go 协程中通过通道 channel 传递数据。
通道使用
1 2 3 4 c := make (chan int ) func worker (c chan int ) { ... } CHANNEL <- DATA VAR := <-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 package mainimport ( "fmt" "math/rand" "time" ) func main () { c := make (chan int ) for i := 0 ; i < 5 ; i++ { worker := &Worker{id: i} go worker.process(c) } for { c <- rand.Int() time.Sleep(time.Millisecond * 50 ) } } type Worker struct { id int } func (w *Worker) process (c chan int ) { for { data := <-c fmt.Printf("worker %d got %d\n" , w.id, data) } }
带缓存的通道
顾名思义,可以指明通道的长度:
1 c := make (chan int , 100 )
通过查看通道的长度,我们可以了解到,带缓存通道中有待处理的缓存数据:
1 2 3 4 5 for { c <- rand.Int() fmt.Println(len (c)) time.Sleep(time.Millisecond * 50 ) }
select
select
用于丢弃通道中的消息,使用时类似于 switch
:
1 2 3 4 5 6 7 8 9 10 for { select { case c <- rand.Int(): default : fmt.Println("dropped" ) } time.Sleep(time.Millisecond * 50 ) }
使用select
的最主要目的是,通过它管理多个通道。给定多个通道,select
将阻塞直到有一个通道可用。如果没有可用的通道,当提供了default
语句时,执行该分支。当多个通道都可用时,选择其中的一个通道是随机的。
超时
我们也可以利用超时来丢弃通道中的信息,使用time.After
函数。
1 2 3 4 5 6 7 8 for { select { case c <- rand.Int(): case <-time.After(time.Millisecond * 100 ): fmt.Println("timed out" ) } time.Sleep(time.Millisecond * 50 ) }
总结
这本书确实是非常浅显地讲解了 Goland 的基本特性,花费了大约四个小时做了一下记录,接下来对于语法需要代码训练实现熟悉,常见包和并发编程等需要阅读源码和博客文章进一步了解。