1. 基础知识
- 每个go程序都是由
package
定义的。package
必须在源文件的第一行。 - 每个可执行程序必须有且只有一个main包,main包里面必须有main函数。
|
|
import
- 多个包可以一个一个import:
1 2 3
import "fmt" import "os" import "log"
- 也可以简化:
1 2 3 4 5 6 7 8 9
import ( "fmt" "os" "log" ) //调用 func main(){ fmt.Println("hello world"); }
- 导入的包没有调用里面的函数等,就会导致编译失败。
- 可以设置别名:
1 2 3 4 5 6
import env "fmt" //调用 func main(){ env.Println("Hello world") }
- 或者可以直接省略调用:使用“
.
”代替别名,调用包函数的时候就不需要使用包名了(但是不推荐这样做)
1 2 3 4 5
import . "fmt" func main(){ Println("hello world") }
- 省略调用和别名不能同时使用
- 可见性规则:使用大小写来决定是
private
或者public
- 大写就是表示
public
,表示在包的外部可以调用 - 小写就是表示
private
,表示只能在包内部被调用
- 多个包可以一个一个import:
2. 类型与变量
2.1. 基本类型
- 布尔类型:
bool
- 长度:1字节
- 取值范围:true、false
- 不能使用c#等其他语言中的0、1来代表
- 整型:
int
/uint
- 根据程序运行平台分为32/64位
- int类型必须是整数,带小数的必须申明为float类型
- 字节型:
byte
/uint8
- 复数:
- 值类型:
array
、struct
、string
- 引用类型:
slice
(切片)、map
(哈希)、chan
(通道,并发编程必不可少)
- 接口类型:
interface
- 函数类型:
func
2.2. 类型默认值
- 值类型默认值为0
- bool默认为false
- string默认为空字符串
2.2.1. 类型的别名
|
|
2.3. 变量的声明与赋值
|
|
|
|
|
|
2.4. 变量的类型转换
- Go语言中所有的类型转换必须是显示声明的强制转换
- 只能转换两种互相兼容的类型:例如int和float之间可以转换,但是int和bool之间转换会发生编译错误
|
|
|
|
|
|
2.5. 常量的定义
- 常量的定义推荐使用全部大写字母声明
- 如果常量不想被外部调用,那就首字母需要是小写或者下划线
- 常量如果使用函数作为值,那么必须是一个内置函数
- 常量赋值必须是一个常量
- 同时声明多个常量,第一个常量必须赋默认值
- 申明多个常量,没有赋初始值的会默认取上一行的值
|
|
- 可以使用常量实现枚举效果
|
|
|
|
2.6. 运算符
2.7. 控制语句
- 控制语句条件没有括号
|
|
- 可以在控制语句中声明变量并使用,但是作用域只在控制语句内
|
|
2.7.1. 循环
- Go语言中只有一个
for
循环关键字,像c#中除了for
还有foreach
、while
关键字 Go语言中的
for
支持3种形式- 无限循环
1 2 3 4 5 6 7 8 9
func mian(){ a := 1 for { a++ if a > 3 { break //必须要有一个条件控制,否则就进入无限循环状态 } } }
- 自带条件表达式
1 2 3 4 5 6 7
func main (){ a := 1 for a < 10 { //TODO a++ } }
- 正常循环语句
1 2 3 4 5
func main(){ for i := 0; i < 10; i++{ //TODO } }
2.7.2. switch
- 不需要写关键字
break
,符合条件就会自动终止switch
|
|
- 如果符合后需要继续下一个
case
,那么就需要显示的使用fallthrough
关键字
|
|
- 支持初始化一个表达式,这时表达式需要以封号结尾
|
|
2.8. Array
- 数组的声明
|
|
- 可以不指定数组的长度
|
|
- 数组在Go语言中为值类型(c#中是作为引用类型的)
- 可以对单个元素的值进行操作
|
|
- 可以定义使用多维数组
|
|
- 冒泡排序
|
|
2.9. 切片Slice
- Slice本身不是数组,它只是指向一个数组的底层地址
- 因为指向的是的内存地址,所以Slice是引用类型
- Slice可以从一个数组获取生成
|
|
- Slice也可以使用函数
make([]T,len,cap)
进行创建[]T
:表示创建一个Slice(也就是不指定长度的数组)len
:长度,表示这个Slice里面包含多少个元素cap
:容量,因为Slice是长度可变的,为了不浪费空间,可以指定一个大致的容量,让内存取分配指定容量的连续内存空间。当实际存储的长度大于容量是,会自动的对容量进行乘2操作,并重新寻找一块新容量大小的连续内存空间。所以在开始应该对容量有一个大致的设定,避免频繁重新创建,带来的性能损耗。cap
可以省略不写,如果cap
省略掉,那么默认就和len
相同
|
|
Slice指向的是一个数组的内存地址,并不是一个独立的数组
- 从图中可以看到,Slice_a 只包含三个元素,实际容量是包含了Array_ori这个数组的c元素开始到最后。
- Slice_a根据内存寻址,可以找到从c开始到Array_ori这个数组结尾的所有元素,但是Slice_a本身只包含了c,d,e三个元素,这也说明了Slice指向的是一个底层数组的引用地址。
- 例如:
1 2
Slice_a[2] //得到“e” Slice_a[4] //得到“g”
Reslice
- 从一个Slice取一个新的Slice
1 2 3 4 5 6 7 8
func main(){ a := [] int{1,2,3,4,5} s1 := a[2:5] //[3,4,5] //s2要取3,4元素 s2 := a[2:4] //可以从元素组中取 //也可以从s1中取,因为s1已经包含3,4这两个元素了 s2 := s1[0:2] //取s1索引0开始到第二个元素就是3,4 }
- Reslice索引不可以超越被Slice的切片的容量
cap()
的值,超过之后会引发编译错误,上面的例子中s2
取值如果这么写就是错误的:s2 := s1[3:6]
,索引3已经超过s1
的最大容量(s1
的最大容量是a数组从3开始到结尾的大小,结合上面的说明理解)
Append
- 可以在Slice的结尾追加元素
- 可以把一个Slice追加到另一个Slice的尾部
1 2 3 4 5
func main(){ s := make([]int,3,5) //长度为3,容量为5的slice s = append(s,1,2) //把1,2两个元素追加到s的尾部,得到[0,0,0,1,2] }
如果最终追加后的长度没有超过slice的容量,那么返回的还是原来的那个slice,如果超过了,那么就会创建一个新的容量的内存空间,把数值复制到新的地址,然后返回的就是新的slice。
- 追加后的slice没有超过原slice的容量
1 2 3 4
func main(){ s := make([]int,5,10) s = append(s,1,2,3) //追加3个元素,总元素为8,没有超过s的容量10,那么新赋值的s,指向的内存地址还是原先s的地址 }
- 追加后的slice超过了远slice的容量
1 2 3 4 5
func main(){ s := make([]int,5,10) s = append(s,1,2,3,4,5,6) //追加了6个元素,超过了原来s的最大容量10,那么就会创建一个新内存空间(大小为容量乘2),将新的s复制进去,所以现在这个s指向的其实是一个新的内存地址了 s1 = s //s1指向的还是原先s的内存地址 }
Copy
将一个slice拷贝到另一个slice中
copy(拷贝到的slice,被拷贝的slice)
- 将长度大的slice拷贝到长度小的slice中
1 2 3 4 5
func main(){ s1 := []int{1,2,3,4,5,6} s2 := []int{7,8,9} copy(s2,s1) //表示将s1拷贝到s2中,由于s1的长度为6,s2的长度为3,那么拷贝后s2就变成了[1,2,3],因为s2的容量只有3,所以就把s1的前三个默认拷贝到s2中去了 }
- 将长度小的slice拷贝到长度大的slice中
1 2 3 4 5
func main(){ s1 := []int{1,2,3,4,5,6,} s2 := []int{7,8,9} copy(s1,s2) //将s2的元素拷贝到s1当中去,拷贝后的s1变成[7,8,9,3,4,5] }
循环slice
|
|
2.10. map
map
类似c#中的Dictionary
,以key-value
对的形式存储数据
|
|
|
|
- map嵌套的时候,需要对每一级的map进行make操作
|
|
- 判断map是否被初始化
|
|
- 循环map
|
|
|
|
|
|
- map的间接排序
|
|
2.11. 函数
- Go语言的函数不支持嵌套、重载和默认参数
|
|
- 不定长变参
|
|
- int、string值类型参数传递
|
|
- slice引用类型作为参数传递,传递的是内存地址的拷贝
|
|
- 传递引用类型,使用指针修改参数本身
|
|
- 函数可以作为类型使用
|
|
- 匿名函数的使用
|
|
- 闭包
|
|
2.11.1. defer
- 类似于其他语言中的析构函数,在函数体执行结束后,按照调用顺序相反的顺序捉个执行一遍
|
|
- 即使函数发生严重的错误,defer也会正常执行
- 支持匿名函数调用
|
|
- Go语言没有异常处理机制,没有c#中
try cache
语句 在Go语言中可以使用
panic
/recover
模式来处理错误panic
可以在任何地方引发错误抛出recover
只能在defer
调用的函数中有效,因为使用recover
的时候错误已经引发了,只能借助于defer
进行一些“恢复”操作recover
可以返回panic
信息
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 32 33 34
func main(){ } func fa(){ fmt.Println("func fa") } func fb(){ //defer如果放在panic 后面,那么就执行不了recover了,因为程序走到panic的时候就已经停止不会往下走了,所以需要在开始就注册recover,在有panic信息抛出,就执行recover defer func(){ if err := recover(); err != nil{ //获取到了panic信息 fmt.Println("recover fb"); } } panic("panic fb") //引发错误 } func fc(){ fmt.Println("func fc") } //在没有写defer的时候最终输出 /* func fa panic fb ... other err info */ //在写有defer recover的时候会输出 /* func fa recover fb func fc */
2.12. 结构struct
- Go语言中没有
class
的概念,而struct
就相当于go语言中的类 struct
是一种类型(type)
|
|
struct
是一个值类型,传递的时候是值拷贝,在其他函数中修改改变不了值本身,希望做改变的时候,可以使用指针传递
|
|
- 匿名结构
|
|
- 嵌套struct
|
|
- 匿名字段
|
|
- 相同类型的struct相互之间可以进行赋值
|
|
- 两个名称不同的struct,尽管它们的字段一致,它们也是两个不同的类型,它们之间不能进行比较个赋值。
- 使用嵌入结构实现类似继承的能力
|
|
嵌入结构,同名字段的处理
- 单层嵌套
1 2 3 4 5 6 7 8 9 10 11 12 13
type sa struct{ sb Name string } type sb struct{ Name string } func main(){ a := sa{ Name : "sa",sb : sb{Name : "sb"}} //a.Name就会找sa中的Namw字段 //a.sb.Name就会找sb中的Name字段 fmt.Println(a.Name,a.sb.Name) }
- 多层嵌入
1 2 3 4 5 6 7 8 9 10 11 12 13 14
type sa struct{ sb sc } type sb struct{ Name string } type sc struct{ Name string } func main(){ a := sa{sb : {Name : "sb"},sc : {Name : "sc"}} fmt.Println(a.Name) //这里会报错,因为sb、sc里面都有Name字段,没有办法判断该调用哪个,可以使用a.sb.Name 、a.sc.Name }
2.13. 方法method
- 相当于是c#中的扩展方法,给某一个类型(type)编写额外的扩展方法,供类型(type)调用
|
|
- 方法同样可以传入指针,对参数本身进行修改
|
|
- 使用底层类型方法
|
|
2.14. 接口interface
- 接口是一个或多个方法签名的集合(类似c#中的接口,只有声明,没有具体的实现)
- 只要某个类型拥有某个接口的所有的方法签名,那么就算是这个类型实现了该接口,而无需显示的声明实现了哪个接口(在c#中实现一个接口是在类的名字后面写上冒号,然后写接口名,go语言中无需这样做)
|
|
- 可以嵌入接口
|
|
- 接口调用
|
|
- 所有的类型的父接口,就是一个空的接口(相当于就是c#中的
System.Object
),理论上来说所有的类型都实现了一个空接口 - 简单工厂
|
|
- 接口之间可以互相转换:按照上面的例子,因为接口
USB
包含了接口Connecter
,所以接口USB
可以转换成Connecter
,但是Connecter
不能转换乘USB
,这里有一个包含和被包含的关系
|
|
2.15. 反射reflection
- 反射使用
TypeOf
和ValueOf
函数从接口中获取目标对象信息。
|
|
输出结果
|
|
- 如果传入反射的参数是一个地址引用,那么使用上面的方法就会出错,可以使用if判断一下
|
|
- 反射匿名、嵌入字段
|
|
输出:
|
|
- 反射修改字段的值
|
|
- 反射修改struct的字段值
|
|
- 反射调用方法
|
|
2.16. 并发concurrency
- goroutine是由官方实现的超级“线程池”,每个实例只有4-5KB的栈内存占用,由于实现机制而大幅减少的创建和销毁“线程”的开销,是go语言可以实现高并发的根本原因。
- 并发不是并行:
- 并行是直接利用多核处理器实现多个核心同时运行多线程的能力,并行是由处理器的核心数决定的,例如单核处理器就实现不了并行。
- 并发是由切换时间片来实现的,每个线程执行一段时间,然后来回切换。
- Go语言可以设置使用并发的核数,可以最大发挥多核处理器的能力。
- 所以Go语言的并发比并行更加高效
- goroutine奉行通过通信来共享内存,而不是共享内存来通信
- 简单理解:goroutine之间是使用channel进行通信的,而不是使用锁机制,所以不需要对两个goroutine之间的切换加锁,只需要使用go语言实现的channel进行通信即可实现共享内存的切换。
- 使用
go
关键字使用:
|
|
上面的代码运行结果并没有跟想象的那样输出一句”hahaha”,而是什么都没有输出,然后就退出了,原因是:我们在main函数中启动了一个goroutine执行Go函数,这时候就由一个线程去执行Go函数了,然后main没什么影响,继续往下执行,下面什么都没有,所以就直接退出了。
|
|
在实际的代码中,我们一般使用channel进行通信,通知其他函数,这个goroutine执行完毕了,然后可以操作其他的了
- channel是goroutine之间通信的桥梁,大都是阻塞同步的
- channel存取值操作:
- 存值使用操作符:
channel<-
(存值时操作符在channel变量的右边) - 取值使用操作符:
<-channel
(取值时操作符在channel变量的左边)
- 存值使用操作符:
1 2 3 4 5 6 7 8 9 10
func main(){ c := make(chan bool) //使用make函数创建一个channel,存储bool 类型的值 //定义一个goroutine匿名函数 go func(){ fmt.Println("hahaha") c <- true //给channel存一个值 }() //在main函数的最后,从channel中取值,所以这里就会等待goroutine中对channel进行存值,然后这里取出来了,才会结束main函数,这里相当一个阻塞线程的操作 <- c }
- channel是通过
make()
创建,close()
进行关闭的 - channel是引用类型
- 可以使用
for range
来不断的迭代操作channel,进行取值操作
1 2 3 4 5 6 7 8 9 10 11 12
func main(){ c := make(chan bool) go func (){ fmt.Println("hahaha") c <- true Close(s) //因为main函数最后有一个for循环在一直迭代取值channel存入的值,所以这里如果不进行Close的话,那么就会发生一个死锁,而抛出异常 }() //这里会等待上面的匿名函数给channel存入一个值,然后打印出来,接着匿名函数调用了Close函数,通知for循环channel关闭,停止迭代,然后main函数继续往下执行,直至退出 for _,v := range c{ fmt.Println(v) } }
- 可以设置双向(读写)和单向通道(只读、只写)
- 可以设置缓存(channel存储数据量)的大小,在没有到最大值的时候,不会发生线程阻塞
1 2 3 4 5 6 7 8
func main (){ c := make(chan bool,3) //创建一个channel,可以存储3个长度的数据 go func(){ fmt.Println("123") c <- true } <- c //取出channel中的数据 }
- 有缓存的channel相当于是非阻塞的,而有进程的是同步阻塞的。
- channel相当于是queue的功能,先进先出。
- 如果创建了一个带缓存的channel,那么在channel还没有存储满的时候,取channel数据这个操作不会阻塞程序的运行。反之就会等待channel中有值可以取出时,才会继续下面的其他操作。
- 看到网上一个解释:把channel比作邮递员,有缓存时,相当于你家门外面有一个信箱,只要没装满,邮递员就直接把信放进去就走,不用管你取不取。 没缓存就是邮递员必须把信交到你手里才能走。