一. 符号
-
=
与:=
-
:=
: 用来初始化一个不存在的变量, 包括声明和初始化2个步骤 -
=
: 赋值符号, 当变量被:=
初始化后, 后续赋值使用=
-
-
package main
下的func main()
为运行入口 -
for
循环- Go只有一种循环: for循环
-
while
可通过for condition
实现 - 死循环:
for{}
- for循环的
range形式
遍历数组:- 符号:
for index,val in range [arr]
- range数组会返回index和元素值
var arr = []int{1,2,3,4} for index,val := range arr { fmt.Println(index,":",val) }
- 符号:
-
if
条件-
if
的分支判断条件像for一样不用小括号括起:if condition
-
if
可以在分支条件前执行任意一个语句:if [one sentense] condition
- 如果在if的分支条件之前初始化了一个变量, 则该变量也作用于else语句块内
if v:=10; v>100{ fmt.Println("haha") }else{ fmt.Println(v) }
-
-
switch variable
条件-
switch case
语句中, case返回第一个匹配到的项, 不用break -
case
可以声明default匹配 - 不写
variable
的switch相当于switch true
, 用来简化其他语言中的if..else if.. else if..
-
-
defer
- 一个函数被
defer
调声明后, 函数中的参数立即求值, 执行却是在return语句之后执行func fun() int{ i:=0 defer fmt.Println("world",i) i++ fmt.Println("hello",i) return i } func main(){ fmt.Println("返回值",fun()) } //hello 1 //world 0 //返回值 1
- 一个函数被
二. 指针与结构体
- 指针
指针保存了变量所在的内存地址-
&
: 生成一个指向变量的指针 - 指针类型
*T
: 称作T的指针类型
.*
代表指针.
aaa := 1 bbb := "aaa" fmt.Printf("%p %p",&aaa,&bbb)
-
当使用
&
操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*
操作符,也就是指针取值.str := "hello world" ptr := &str fmt.Printf("ptr type: %T\n", ptr) // 打印ptr的类型 fmt.Printf("address: %p\n", ptr) // 打印ptr指向的地址, value := *ptr // 对指针取值 fmt.Printf("value type: %T\n", value) fmt.Printf("value: %s\n", value) // ptr type: *string // address: 0xc0420461c0 // value type: string // value: hello world
-
通过指针修改值
*
操作符的根本意义就是: 操作指针指向的变量。当出现在等号右边时,就是取指向变量的值,当出现在等号左边时,就是将值设置给指向的变量。func swap(a,b *int){ t := *a *a = *b *b = t } func main(){ x,y := 1,2 swap(&x,&y) fmt.Println(x,y) }
如果在
swap()
函数中交换操作的是指针值,会发生什么情况?可以参考下面代码:func swap(a,b *int){ a,b = b,a } func main(){ x,y := 1,2 swap(&x,&y) fmt.Println(x,y) // 1,2 }
结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。
-
创建指针的另一种方法——new() 函数
- Go语言还提供了另外一种方法来创建指针变量,格式如下:
new(类型)
str := new(string) *str = "go go go" fmt.Println(*str)
-
new(type)
创建指针的方式, 可以避免由于修改一个空指针的指向而编译出错, 如下代码揭示了这一现象
var str2 *string *str2 = "gogogo" // panic: runtime error: invalid memory address or nil pointer dereference fmt.Println(*str2)
这个问题出现的原因是, 指针类型的数据起初始值是
nil
, 而nil
没有具体指向, 也就不能更改器指向. 要想修复这问题, 就要使用上一个代码片段中的new(type)
进行声明 - Go语言还提供了另外一种方法来创建指针变量,格式如下:
-
方法中的指针
方法即为有"receiver"的函数. 这里的"reciever"指调用该函数的结构体实例. "reciever"既可以是实例变量也可以是指针类型的变量. 但二者在调用效果上存在差别:-
value method
: "reciever"为实例的方法- 符号:
func (T) method(param) rtype{}
- 值方法既可以被实例调用也可以被指针调用, 但是这种方式即使在方法内部有修改reciever值的代码, 也不能在函数退出后永久性的修改reciever
type Person struct{ name string age int } func(p Person) say(){ fmt.Println(p.name,":",p.age) } func (p Person) changeAge(newAge int){ p.age = newAge } func main() { p := Person{"zhangsan",23} (&p).changeAge(28) // 指针调用值方法 p.say() // 23 p.changeAge(28) // 实例调用值方法 p.say() // 23 }
- 符号:
-
pointer methd
: "reciever"为指针的方法- 符号:
func (*T) method(param) rtype{}
- pointer method被指针类型的变量调用, 因为pointer method的本质是要更改reciever的值, 如果用value类型的变量调用就会导致值拷贝而无法该值, 这会造成语义上的冲突. 为了解决这个问题, 编译器如果发现是一个value型变量调用了
pointer method
, 编译器会自动在前面加上&
, 改写成(&value)
. 如下代码所示
type Person struct{ name string age int } func(p Person) say(){ fmt.Println(p.name,":",p.age) } func (p *Person) changeAge(newAge int){ p.age = newAge } func main() { p := Person{"zhangsan",23} (&p).changeAge(28) // 指针调用指针方法 p.say() // 28 p.changeAge(29) // 实例调用指针方法 p.say() // 29, 编译器会自动将该调用改写成上面的(&p).changeAge(28) }
- 符号:
-
-
- 结构体
- 结构体是一组字段, 用
type [struct name] struct
来声明 - 使用
.
访问结构体的某一个字段 - 用
TypeName{Fileld1 \n Field2}
来创造结构体对象type Vertex struct{ X int Y int }
- 可以只对结构体的某几个字段赋值, 其他几个字段使用结构体字段类型的默认值.
structname{Field1: value}
- 结构体是一组字段, 用
三. 数组与切片
-
数组
- 数组作为一种类型出现, 声明数据时的使用方法和基本数据类型相同. 用
[n]T
表示拥有n个T类型的值的数组类型,arr[n]
对数组元素赋值或取值var arr [2]string arr[0] = "hello" arr[1] = "world" fmt.Println(arr[0],arr[1])
-
{}
一次性声明数组全部元素a := []int {1,2,3}
- 结构体数组
a := [] struct{ a int b bool }{ {1,false}, {2,true}, } fmt.Println(a) // [{1 false} {2 true}]
- 数组作为一种类型出现, 声明数据时的使用方法和基本数据类型相同. 用
-
切片
切片
: 数组的切片也是一种类型, 用[]T
表示一个元素类型为T的切片. 通过数组的上下界对数组切片, 使用arr[low:high]
切片, 切片区间是左闭右开的- 切片与数组共享底层的数据
- 切片的长度获取:
len(slice)
- 切片的容量, 是切片第一个元素到底层数组末尾的元素个数 :
cap(slice)
- 切片的
零值
或者说是默认值
是nil
, 长度为0, 切没有底层数组 -
[陷阱]
: 切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。 因此有时候可能会因为一个小的切片引用导致保存所有的数组数据, 导致 GC 不能释放数组的空间要修复整个问题,可以将感兴趣的数据复制到一个新的切片中:dest := make([]byte, len(src)) copy(dest, src)
-
make()
创建数组和切片
用make(slice type, length, [capacity])
创建切片a:=make([]int,5) // len(a)=5 b:=make([]int,0,5) // len(b)=0, cap(b)=5
-
二维数组
- 二维数组类型:
[][] ElemType
var slice = [][]int{ {1, 2, 3}, {2, 3, 4}, }
- 二维数组类型:
四. Map类型
-
make(type)
创建Mapvar m = make(map[string]Vertex) m["a"] = Vertex{1,2}
- 声明时初始化赋值
func main(){ var m = map[string]Vertex{ "a":{1,1}, "b":{2,2,}, } }
- 删除key
delete(m,"a")
五. 函数
- 函数的闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。
func adder(x int) func(int) int {
sum := x
return func(y int) int{
sum += y
return sum
}
}
func main(){
myfunc := adder(1)
fmt.Println(myfunc(2))
}
- go没有类
go通过为结构体增加方法来实现类的作用.func (structVariable StructType) functionName(field) returnType{ 可以引用structVariable中的属性 }
六. 接口
-
接口是隐式实现的, 因此没有显示的implements关键字
只要一个结构体中包含接口的方法, 就自动认为该结构体的实例是某个接口的实现(当然, 要实现接口的全部代码) -
符号
从语法上看,Interface定义了一个或一组method(s),这些method(s)只有函数签名,没有具体的实现代码type iname interfase{ method signature() }
type I interface{ M() } type T struct { S string } func(t T) M() { fmt.Println(t.S) } func main() { var i I = T{"hello"} i.M() }
-
类型断言
(type assertion
)- 符号:
val.(type)
, 断言变量val的类型是type - 只能对实现接口的结构体进行断言
- 符号:
七. Goroutine与Channel
-
基本理解
-
Goroutine
是Go中最基本的执行单元。事实上每一个Go程序至少有一个goroutine:主goroutine。当程序启动时,它会自动创建。 -
Channel
: goroutine是Go语言的基本调度单位,而channels
则是它们之间的通信机制。操作符<-
用来指定管道的方向,发送或接收。如果未指定方向,则为双向管道。
-
-
Channels pipeline
第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数func counter(out chan<- int, number int) { for x := 0; x < number; 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.Print(v,",") } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals,10) go squarer(squares, naturals) printer(squares) }
-
range
和close
- 发送者可通过
close(chann)
关闭一个信道来表示没有需要发送的值了。 - 接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭, 则在执行
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++{ // 将x发送到信道, 也意味着这个方法会在一个新的goroutine中执行 c <- x x,y = y,x+y } close(c) // 由发送者关闭信道 } func main(){ loop := 10 c := make(chan int) go fibonacci(loop,c) // 创建goroutine for i:= range c{ // i为从信道接受的值 fmt.Print(i,",") // 0,1,1,2,3,5,8,13,21,34, } }
- 发送者可通过
-
select{case}
-
case
语句块是select
可选择的执行分支. 他会随机选择一个不会产生阻塞的分支进行处理.如果所有的case
分支都会阻塞, 就会选择走case default
分支进行; 如果没有default分支切所有其他分支均阻塞, 则select
语句阻塞
func fibonacci(c chan int, quitChan chan int){ x,y := 0,1 for { select { case c<-x: // 将x发送到信道 x,y = y,x+y case <-quitChan: fmt.Println("quit fibonacci") return } } } func main(){ c := make(chan int) quitChan := make(chan int) loop := 10 go func() { for i := 0; i < loop; i++ { fmt.Print(<-c,",") } quitChan <- 0 // 循环得到10次结果后, 发送退出信号 }() fibonacci(c,quitChan) // 此时上面的goroutine中的select才能继续进行 // 0,1,1,2,3,5,8,13,21,34,quit fibonacci }
-
-
sync.Mutex
我们已经看到信道非常适合在各个Goroutine间进行通信。但是如果我们并不需要通信呢?比如只是想保证每次只有一个Goroutine能够访问一个共享的变量,从而避免冲突?
这里涉及的概念叫做互斥(mutual exclusion)
,我们通常使用互斥锁(Mutex)
这一数据结构来提供这种机制。
Go 标准库中提供了sync.Mutex
互斥锁类型及其两个方法:- Lock
- Unlock
我们可以通过在代码前调用
Lock
方法,在代码后调用Unlock
方法来保证一段代码的互斥执行。参见 Inc 方法。
我们也可以用defer
语句来保证互斥锁一定会被解锁。参见 Value 方法。type SafeCounter struct { v map[string]int mux sync.Mutex } /** 该方法在goroutine中运行*/ func(counter *SafeCounter) Inc (key string){ counter.mux.Lock() counter.v[key]++ counter.mux.Unlock() } /** 该方法在goroutine中运行*/ func (counter *SafeCounter) Value (key string) int{ counter.mux.Lock() defer counter.mux.Unlock() return counter.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")) }
八. 异常
-
异常不是抛出来的, 而是作为方法的其中一个返回值
- 通常, 异常都是内置的
error
借口的具体类型
type error interface { Error() string }
- 模仿
os.PathError
, 自定义异常
type MyError struct{ msg string } // 意味着指针类型*MyError,才是error接口的具体类型 func (err *MyError) Error() string{ return err.msg } func run(s int) (string,error){ if s==0{ return "",&MyError{"hello error"} } return "safe",nil } func main(){ //param := 1 // 最后打印 运行正常, 得到: safe param := 0 // 最后打印 发生了MyError异常: hello error res, err := run(param) if err==nil{ fmt.Println("运行正常, 得到:",res) return } e,ok := err.(*MyError) // 使用断言 if(ok){ fmt.Println("发生了MyError异常:",e.msg) }else{ fmt.Println("发生未知异常") } }
- 一个例子
for try := 0; try < 2; try++ { file, err = os.Create(filename) if err == nil { return } if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC { deleteTempFiles() // Recover some space. continue } return }
- 通常, 异常都是内置的
-
panic
- 发生panic后, 整个程序退出
- panic表示运行时异常. 比如: 切片的index越界, 类型断言失败.
- panic发生后, 会立刻停止当前方法的运行, 释放goroutine的栈, 如果释放后到达了goroutine的栈顶, 程序失败
user := os.Getenv("USER") fmt.Println("user:", user) if user==""{ panic("no value for $USER") } fmt.Println("program continue") // 无法执行, 发生panic后整个程序退出
-
recover
如果某个方法可能会抛出一个panic的异常,可以在调用这个方法之前, 定义一个defer方法, 在defer方法内部使用recover
捕获这个异常,然后继续处理。func say() int{ fmt.Println("a") fmt.Println("b") panic(55) fmt.Println("c") // 这句无法到达, 因为上面已经发生了panic异常 return 999 } func main(){ defer func(){ if err:=recover(); err!=nil{ fmt.Println("occur panic:",err) } fmt.Println("defer recover finish, exec go on .. ") }() res := say() fmt.Println(res) // say()还未执行完就会发生panic异常, 调到defer处被捕获, 所以这句无法到达 } //a //b //occur panic: 55 //defer recover finish, exec go on ..
网友评论