Go语言的源码文件有三大类,即:命令源码文件、库源码文件和测试源码文件。他们的功能各不相同,而写法也各有各的特点。
- 命令源码文件总是作为可执行的程序的入口。
- 库源码文件一般用于集中放置各种待被使用的程序实体(全局常量、全局变量、接口、结构体、函数等等)
- 测试源码文件主要用于对前两种源码文件中的程序实体的功能和性能进行测试。
/**
关键字
package import
select switch case fallthrough default
go defer goto
map chan struct
for range continue break
if else
var type func const return
内建常量
true false iota nil
内建类型
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
float32 float64
bool byte rune string error
uintptr
内建函数
make 初始化内置的数据结构,slice、map、channel。
new 作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针
append 可以为切片动态添加元素
close 关闭了一个通道,这个通道必须是双向的或只发送的。
panic
recover
copy len cap delete
*/
// 声明
var intA int = 9
var stringA string = "abc"
var floatA float32 = 9.87
// 批量声明
var i, j, k int
// 简短变量声明,简短变量声明被广泛用于大部分的局部变量的声明和初始化。
intC := 9
//批量初始化
i, j := 0, 1
var x int = 1
v := 1
v++ // 等价方式 v = v + 1;v 变成 2
v-- // 等价方式 v = v - 1;v 变成 1
//元组赋值
x, y = y, x
//隐式的赋值行为
medals := []string{"gold", "silver", "bronze"}
- 一个类型可以看作是值的模板。一个值可以看作是某个类型的实例。
uintptr
类型的值的尺寸能够存下任意一个内存地址。
布尔类型:bool
整形:byte int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr
浮点:float32 float64
字符:rune
字符串:string
错误类型:error
type Duration int64
-
Go 中有两种内置类型别名
byte
是uint8
的内置别名rune
是int32
的内置别名
-
布尔值有两种状态使用预声明的常量(
false
和true
)来表示。 -
类型别名声明
type Name = string
-
任何类型的指针零值都是nil。
-
指针可以被赋值为nil,但是不能被赋值为其他类型的值。
-
指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
var x, y *int fmt.Println(x,y) fmt.Println(x == x, x == y, x == nil) //true true true
-
在 Go 中返回一个局部变量的地址是安全的。编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。
func newInt() *int { c := 3 return &c }
无类型常量和有类型常量,无类型常量灵活度更高。
package main
import "fmt"
const hello = "hello, world"
//字符串类型 string 起了别名后是一个新的字符串类型,如 typeHello,尽管意义是一样的,但是在规则上,是不同的类型
const typeHello string = "Hello, World"
func main() {
type myString string
var a myString
a = hello //这样是可以的
a = typeHello //这样是错误的
fmt.Println(a)
}
// 类似 C 语言中的 for 用法
for init; condition; post { }
// 类似 C 语言中的 while 用法
for condition { }
// 类似 C 语言中的 for(;;) 用法
for { }
//eg:1
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
//eg:1
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
//eg:2 switch 并不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
//eg:3 类型断言
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T 打印任何类型的 t
case bool:
fmt.Printf("boolean %t\n", t) // t 是 bool 类型
case int:
fmt.Printf("integer %d\n", t) // t 是 int 类型
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t 是 *bool 类型
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t 是 *int 类型
}
- 数组是值。将一个数组赋予另一个数组会复制其所有元素。
- 特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
- 数组的大小是其类型的一部分。类型
[1]int
和[2]int
是不同的。
// 声明
var q [3]int = [3]int{1,2,3}
// 如果在数组的长度位置出现的是 ... 省略号,则表示数组的长度是根据初始化值的个数来计算。
q := [...]int{1,2,3}
- 切片的零值为
nil
。对于切片的零值,len
和cap
都将返回0。 - 切片是一种数据结构,描述与切片变量本身分开存储的数组的一段连续的部分;切片不是数组,切片描述一块数组。
//声明
var s1 []int
// 简短声明
s2 := []int{0,1,2,3,4,5}
// make 创建一个指定元素类型、长度和容量的slice,容量部分可以省略。
// 在底层,make 创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的 slice 才能引用底层匿名的数组变量。
s3 = make([]int,3)
s4 = make([]int,3,4)
//slice 唯一合法的比较
if summer == nil {}
//append 函数,内置函数用于向 slice 函数追加元素
s1 := append(s2,6,7,8)
/*
1. 切片也可以基于现有的切片或数组生成
2. 切分的范围由两个由冒号分割的索引对应的半开区间指定
3. 切片的开始和结束的索引都是可选的;它们分别默认为零和数组的长度。
*/
b := []byte("golang") // or b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
fmt.Printf("%s\n",b[1:4]) //ola
fmt.Printf("%s\n",b[:]) //golang
//字符串:它们只是只读的字节切片。
usr := "/usr/ken"[0:4]
slice := []byte(usr) //转换
//声明
type tree struct {
value int
left, right *tree
}
//声明
func name(parameter-list) (result-list){
}
//遇到没有函数体的函数声明
package math
func Sin(x float64) float
//匿名函数
func squares() func() int{
var x int
return func() int {
x++
return x * x
}
}
//可变参数
func sum(vals ...int) int{
total := 0
for _,val := range vals {
total += val
}
return total
}
fmt.Println(sum(1, 2, 3, 4))
- defer 的执行顺序与声明顺序相反,越后面的
defer
函数越先被执行。 - 当执行到(defer)该语句时,函数和参数表达式得到计算,但直到包含该
defer
语句的函数执行完毕时,defer
后的函数才会被执行,不论包含defer
语句的函数是通过return
正常结束,还是由于panic
导致的异常结束。
//eg.1
func main() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
}
//result 4 3 2 1
//eg.2
func f()(r int){
defer func(r int){
r = r + 5
}(r)
return 1
}
// result 1
//eg.3
func f()(result int){
defer func(){
result ++
}()
return 0
}
//result 1
//eg.4
func f()(r int) {
t :=5
defer func(){
t = t + 5
}()
return t
}
//result 5
- panic 是一个内置函数,它用来停止正常的流程并使程序处于 panicking 状态。当函数F调用 panic 时,F 函数立即停止执行,然后运行在 F 中定义的所有 defer 函数并返回调用者,对于调用者来说,F 函数的行为就像 panic 一样。该进程继续向上执行,直到当前 goroutine 中的所有函数都返回,此时程序奔溃。
- Recover 是一个内置函数,可重新控制 panicking 状态下的 goroutine。Recover只在延迟函数(defer)内部有用。在正常执行期间,调用 recover 将返回nil,并且没有其他效果。如果当前 goroutine处于 panicking,调用 recover 将捕获指定给 panic 的值并恢复正常执行。
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
*/
-
方法能给用户定义的类型添加新的行为。
-
可以为任何已命名的类型(除了指针或接口)定义方法。
//slice
type mySlice []int
func (s mySlice) Len() int {
return len(s)
}
//string
type myStr string
func (dist myStr) strJoin(src myStr) string {
return string(dist + src)
}
func TestFunc(t *testing.T) {
var s mySlice
fmt.Println(s.Len())
var strA myStr = "hello"
fmt.Println(strA.strJoin("world"))
}
Go 语言里有两种类型的接收者:值接收者和指针接收者。
type user struct {
name string
email string
}
//值接收
func (u user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n", u.name,u.email)
}
//指针接收者
func (u *user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n", u.name,u.email)
}
- 在 Go 语言中,每一个并发的执行单元叫作一个 goroutine。
- 主函数(main)返回时,所有的 goroutine 都会被直接打断,程序退出。
//创建 channel;
ch := make(chan int)
// or
var ch chan int
ch = make(chan int)
// 发送操作
ch <- 9
//接收操作
x := <-ch
<-ch
//内置函数 close 函数关闭一个channel
// 1. 重复关闭 channel 将导致 panic 异常
// 2. 只有当需要告诉接收者 goroutine,所有的数据已经全部发送才需要关闭 channel。
// 3. 不管一个 channel 是否被关闭,当它没有被引用时将会被 go 语言的垃圾自动回收器回收。
close(ch)
//channel 的缓存
ch = make(chan int) //无缓存
ch = make(chan int,3) //有缓存
一个基于无缓存 Channels 的发送操作将导致发送者 goroutine 阻塞,直到另一个 goroutine 在相同的 Channels 上执行接收操作,当发送的值通过 Channels 成功传输之后,两个 goroutine 可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者 goroutine 也将阻塞,直到有另一个 goroutine 在相同的 Channels 上执行发送操作。
//死锁,不能成功运行
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 9
receive := <-ch
fmt.Println(receive)
}
//管道的实现,即一个 Channel 的输出作为下一个 Channel 的输入。
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
go func() {
for x := 0; x < 10; x++ {
naturals <- x
}
close(naturals)
}()
go func() {
//使用 range 方式,它依次从 channel 接收数据,当 channel 被关闭并且没有值可接收时跳出循环。
for x := range naturals {
squares <- x * x
}
close(squares)
}()
for x := range squares {
fmt.Println(x)
}
}
/**
1. chan<- int 表示一个只发送 int 的 channel,只能发送不能接收。
2. <-chan int 表示一个只接收 int 的 channel,只能接收不能发送。
*/
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(naturals, squares)
printer(squares)
}
func counter(naturals chan<- int) {
for x := 0; x < 10; x++ {
naturals <- x
}
close(naturals)
}
func squarer(naturals <-chan int, squares chan<- int) {
//使用 range 方式,它依次从 channel 接收数据,当 channel 被关闭并且没有值可接收时跳出循环。
for x := range naturals {
squares <- x * x
}
close(squares)
}
func printer(squares <-chan int) {
for x := range squares {
fmt.Println(x)
}
}
-
带缓存的 Channel 内部持有一个元素队列。队列的最大容量是在调用 make 函数创建 channel 时通过第二个参数指定的。
ch := make(chan string, 3)
-
向缓存 Channel 的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个 goroutine 执行接收操作而释放了新的队列空间。相反,如果 channel 是空的,接收操作将阻塞直到有另一个 goroutine 执行发送操作而向队列插入元素。
//无阻塞的发送数据 ch <- "hello" ch <- "world" ch <- "test"
-
获取 channel 内部缓存的容量。
fmt.Println(cap(ch))
-
Goroutines 泄漏
//多个 goroutines 并发向同一个 channel 发送数据。 func mirroredQuery() string { responses := make(chan string, 3) go func() { responses <- request("asia.gopl.io") }() go func() { responses <- request("europe.gopl.io") }() go func() { responses <- request("americas.gopl.io") }() return <-responses // return the quickest response } func request(hostname string) (response string) { /* ... */ } //如果使用了无缓存的 channel,那么两个慢的 goroutines 将会因为没有人接收而被永远卡住。这种情况称为 goroutines 泄漏。
select
也能够让 Goroutine 同时等待多个 Channel 可读或者可写。select
是 go 中的一个控制结构。每个 case 必须是一个通信操作,要么是发送要么是接收。select
随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。
func main() {
ch := make(chan int)
select {
case i := <-ch:
println(i)
default:
println("default")
}
}
一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的。即可以在外部使用的。
- 所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。
- 禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。
- 编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件。
例如,math/rand包的每个源文件的开头都包含package rand
包声明语句,所以当你导入这个包,你就可以用rand.Int
、rand.Float64
类似的方式访问包的成员。
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}
//第一种方式
import "fmt"
import "os"
//第二种方式
import (
"fmt"
"os"
)
//同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。
//导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。
import (
"crypto/rand"
mrand "math/rand"
)
在满足下列条件时,已被声明的变量 v
可出现在 :=
声明中
- 本次声明与已声明的
v
处于同一作用域中(若v
已在外层作用域中声明过,则此次声明会创建一个新的变量 )。 - 在初始化中与其类型相应的值才能赋予
v
。 - 在此次声明中至少另有一个变量是新声明的。
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
在操作系统提供的内核线程之上,Go 搭建了一个特有的两级线程模型。goroutine
代表着可以并发执行的 Go
代码片段。
- M(machine):一个M代表一个内核线程,或称"工作线程"。
- P(processor):一个P代表执行一个
Go
代码片段所必需的资源。 - G(goroutine):一个G代表一个Go代码片段。
一个G的执行需要 P 和 M的支持。一个M在与一个 P 关联之后,就形成了一个有效的 G 运行环境(内核线程+ 上下文环境)。每个 P 都会包含一个可运行的 G 的队列。该队列中的 G 会被依次传递给本地 P 关联的 M,并获得运行时机。
-
为了从任意目录运行
GO
相关命令,GO官方工具链安装目录下的bin
子目录路径必须配置在PATH
环境变量中。 -
GOPATH
此环境变量的默认值为当前用户的HOME
目录下的名为go
文件夹对应的目录路径。 -
GOPATH
文件夹中的pkg
子文件夹用来缓冲被本地项目所依赖的GO模块。 -
GOBIN
环境变量用来指定go install
子命令产生的GO应用程序二进制可执行文件应该存储在何处。他的默认值为GOPATH
文件夹中的bin
子目录所对应的目录路径。GOBIN路径需配置在PATH
环境变量中,以便从任意目录运行这些GO应用程序。/* /home/machunyu/go 默认的 GOPATH 目录 /home/machunyu/go/pkg 来缓冲被本地项目所依赖的GO模块。 /home/machunyu/go/bin 二进制的可执行文件 1. $GOPATH 用来指定当前工作目录 2. GOROOT 用来指定Go的安装目录 3. 工作区目录 GOPATH/ src/ src子目录用于存储源代码 bin/ bin子目录用于保存编译后的可执行程序 pkg/ pkg子目录用于保存编译后的包的目标文件 /*
-race
命令行标志,可以检查。- 只能在运行代码实际触发竞态条件时才检测竞态条件。
go test -race mypkg //测试程序
go run -race mysrc.go //编译并运行程序
go build -race mycmd //构建命令
go install -race mypkg //安装程序包。
func main() {
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
}
func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}