Go学习
《Go语言编程》 许式伟 吕桂华 著
一、语言特性
垃圾回收、更丰富内置类型、函数多返回值、错误处理、匿名函数和闭包、类型和接口、并发编程、反射、语言交互性
二、顺序编程
2.2 常量
- 如果定义常量时没有指定类型,那么它与字面量一样,是无类型常量。
const(
size int 64 = 1024
eof = -1
)
- iota 在每个const关键词出现时被重置为0,然后没出现一次iota,其所代表数字会增加1
const(
c0 = iota //c0 == 0
c1 //c1==1
c2 //c2==2
)
- 枚举:
Go不支持明确的enum关键字。
下面是一个常规的枚举表示法:
const(
Sunday = iota
Monday
Tuesday
Wednesday
...
)
以大写字母开头的常量在包外可见
2.3类型
对于常规开发来说,用int和uint就可以了,以免导致移植困难
字符串在Go中也是基本类型,字符串的内容不能在初始化后被修改
- 字符串遍历:
str := "hello world"
n := len(str)
for i:=0; i<n; i++{
ch := str[i]
fmt.Println(ch)
}
for _,v := range myArray{
fmt.Print(v)
}
-
字符类型
Go中支持两个字符类型,一个是Byte,一个是rune -
数组
在Go中,数组是值类型,所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。若数组作为函数的参数类型,则在调用时函数体中无法修改传入数组的内容。若想修改,需使用切片来实现。 -
切片
类似于C++中数组和vector的关系。
基于数组创建切片:
var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
var mySlice []int = myArray[:5]
#遍历切片
for _, v := range mySlice{
fmt.Print(v)
}
直接创建:
mySlice := make([]int, 5) //初始元素个数为5的切片,初始值为0
mySlice := make([]int, 5, 10) //初始元素个数为5的切片,初始值为0,并预留10个元素的存储空间
mySlice := []int{1,2,3,4,5}
在切片后附加元素:
mySlice = append(mySlice, 1, 2, 3) //附加1,2,3
mySlice = append(mySlice, mySlice...) //...相当于把mySlice打散了传进去
内容复制:
copy() :若两个切片不一样,就会按较小的那个数组切片的元素个数进行复制
- map
GO中map也是基本数据类型
#声明
var myMap map[string] PersonInfo //PersonInfo是一个结构体,也是value的类型
#创建
myMap = make(map[string] PersonInfo)
# 赋值
myMap["1234"] = PersonInfo{初始化}
# 删除
delete(myMap,"1234")
#查找
value, ok := myMap["1234"]
if ok{
处理value
}
2.4流程控制语句
- 跳转语句
goto 标签
2.5函数
小写字母开头的函数只在包内可见,大写字母开头的函数才能被其他包使用(前提是在导入了包的情况下)
func Add(a int, b int) (ret int, err error){
if a<0 || b<0 {
err = errors.New("error")
return //在函数中执行不带任何参数的return语句时,会返回对应的返回值变量的值
}
return a+b, nil
}
- 不定参数
func myFun(args ...int){
for _, v := range args{
fmt.Print(v)
}
}
不定参数的传递:
myfun(args...)
myfun(args[1:]...) //任意的int slice都可以传进去
任意类型不定参数的传递:
func Printf(format string, args ...interface{}){
//...
}
用interface{}传递任意类型数据是GO语言的惯例用法
- 匿名函数和闭包
在Go中,函数可以像普通变量一样被传递或使用
匿名函数可以直接赋值给一个变量或者直接执行
闭包可以非常灵活地操作外部变量,因此在Go语言中,闭包常用于创建函数变量、实现函数式编程、并发编程等场景。
2.6 错误处理
- error接口
type error interface{
Error() string
}
对于大多数函数,若要返回错误,可定义为如下模式:
将error作为返回值的最后一个
func Foo(param int) (n int, err error){}
#调用代码时:
n, err := Foo(0)
if err != nil{
// 错误处理
} else {
// 使用返回值n
}
- 定义自己的error类型
首先,定义一个用于承载错误信息的类型
type PathError struct {
Op string
Path string
Err error
}
然后,实现Error()方法
func (e *PathError) Error() string{
return e.Op + "" + e.Path + "" + e.Err.Error()
}
返回err时,只需要将其他函数返回的普通的err当作参数用于初始化PathError,然后就可以直接返回PathError变量了。
- defer
在当前函数退出前执行defer的操作,defer遵循先进后出的原则
defer srcFile.Close()
# 可以加一个匿名函数处理
defer func(){
}()
- panic() 和 recover()
报告和处理运行时错误和程序中的错误场景
panic 用于引发运行时错误。可以调用 panic 函数来中止程序的正常执行。panic 会导致程序立即停止,并触发执行函数调用栈的逆序执行,直到所有被推迟(defer)的函数调用执行完毕,然后程序退出。
recover 用于捕获 panic 引发的异常,并在程序中恢复。它只能在延迟函数中调用,用于恢复 panic 引发的错误。
三、面向对象编程
3.1类型系统
- 为类型添加方法
Go语言中的大多数类型都是值语义。可以给任何类型,包括内置类型增加新方法。任何类型都可以被Any类型引用,Any类型就是空接口,即interface{}
通过为int取别名并增加了方法,他就变成了一个全新的类型,但这个新类型又完全拥有int的功能:
type Integer int
func (a Integer) Less(b Integer) bool {
return a<b
}
只有在需要修改对象的时候,才必须使用指针
func (a *Integer) Add (b Integer){
*a += b
}
-
值语义和引用语义
GO中数组切片,map,channel,接口是引用语义 -
结构体
Go的结构体和其他语言的类具有同等地位。因为所有的Go语言类型(指针除外)都可以有自己的方法,所以,结构体只是很普通的符合类型。
3.2 初始化
rectPtr := new(Rect) //使用new关键字创建结构体指针
rect2 := &Rect{} //也是指针,加了&的都是指针,不加&就直接返回对象
rect3 := &Rect{0,0,100,200}
rect4 := &Rect{width:100, height:200}
在Go中无构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命名
func NewRect(x,y,width,height float64) *Rect{
return &Rect{x,y,width,height}
}
3.3匿名组合
Go也提供了继承,但采用了组合的文法,所以将其称为匿名组合。
// 定义一个Person结构体
type Person struct {
Name string
Age int
}
// 定义一个Student结构体,匿名组合了Person结构体
type Student struct {
Person
Grade int
}
func main() {
// 创建一个Student对象
student := Student{
Person: Person{
Name: "Alice",
Age: 20,
},
Grade: 10,
}
// 访问Student对象的字段
fmt.Println("Name:", student.Name) // 输出: Name: Alice
fmt.Println("Age:", student.Age) // 输出: Age: 20
fmt.Println("Grade:", student.Grade) // 输出: Grade: 10
}
//Student 结构体匿名组合了 Person 结构体。这意味着 Student 类型继承了 Person 类型的所有字段和方法。
- 方法重写
当结构体通过匿名组合嵌套了多个结构体,且这些结构体有相同方法名时,可进实现重写效果
3.5 接口
在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口
3.5.3 接口赋值
有两种情况:将对象实例赋值给接口(赋值时对象前加&)、将一个接口赋值给另外一个接口(方法列表为其子集)
3.5.4接口查询
接口查询是指通过类型断言或类型判断来判断一个值是否实现了特定的接口,并获取其实现的接口类型或调用接口方法。
- 类型断言
类型断言用于将一个接口类型的值转换为其他具体类型。如果转换成功,可以访问其特定类型的方法和属性。
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
var shape Shape
circle := Circle{Radius: 5.0}
shape = circle
// 类型断言
if c, ok := shape.(Circle); ok {
fmt.Printf("Type assertion successful. Radius: %.2f\n", c.Radius)
} else {
fmt.Println("Type assertion failed")
}
}
- 类型判断
类型判断用于根据不同的类型执行相应的代码块。
func calculateArea(shape Shape) {
switch v := shape.(type) {
case Circle:
fmt.Printf("Area of circle: %.2f\n", v.Area())
default:
fmt.Println("Unknown shape")
}
}
func main() {
circle := Circle{Radius: 5.0}
calculateArea(circle)
}
3.5.6 接口组合
3.5.7 Any类型
由于Go中任何对象实例都满足空接口interface{} 所以,interface{}看起来像是可以指向任何对象的Any类型。
当函数可以接受任意的对象实例时,我们会将其声明为interface{}
func Printf(fmt string, args ...interface{})
四、并发编程
4.1 并发基础
并发的几种主流的实现模型:
- 多进程
开销最大,好处在于简单、进程间互不影响 - 多线程
比进程开销小,但其开销依旧较大 - 基于回调的非阻塞/异步IO
使用多线程模式会很快耗尽服务器的内存和CPU资源。而这种模式通过事件驱动的方式使用异步IO,使服务器持续运转,且尽可能地少用线程,降低开销。但是编程复杂。 - 协程
本质上是一种用户态线程,不需要操作系统来进行抢占式调度,系统开销极小,可以有效提高线程的任务并发性。使用协程的优点是编程简单,结构清晰;缺点是需要语言支持。
4.2 协程
Go语言在语言级别支持轻量级线程,叫goroutine。其让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。
4.3 goroutine
goroutine是Go语言中轻量级线程实现,由Go运行时管理。
加上go
关键字,这次调用就会在一个新的goroutine中并发执行,当被调用的函数返回时,这个gorontine也就结束了。如果这个函数由返回值,其返回值会被抛弃。
4.4 并发通信
Go语言以消息机制而非共享内存作为通信方式
消息机制认为每个并发单元是自包含的,独立的个体,并且都有自己的变量,但在不同的并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。类似于进程的概念,每个进程不会被其他进程打扰。
这种消息机制被称为Channel。不要通过共享内存来通信,而应该通过通信来共享内存。
4.5 Channel
channel 是Go语言在语言级别提供的goroutine间的通信方式。channel是进程内的通信方式
。如果需要跨进程通信,我们建议用分布式系统的方法来解决,比如使用Socket或者HTTP等通信协议。
channel是类型相关的,一个channel只能传递一种类型的值。
- 基本语法
var chanName chan ElementType
var m map[string] chan bool //声明一个map,其元素类型是bool的channel
# 声明并初始化
ch := make(chan int)
ch<-value
value := <-ch
向channel中写入数据会导致程序阻塞,直到有其他goroutine从这个channel中读取数据。如果channel之前没有写入数据,那么从channel中读数据也会导致程序阻塞。
- select
每个case语句里必须是一个channel操作
select {
case <-chan1:
... //如chan1成功读到数据,则进行该case处理语句,并直接忽略掉读到的数据
case chan2<- 1:
... //如果成功向chan2写入数据,则进行该case语句
default:
... //上面都没有成功,进入default
}
在 Go 语言的 select 语句中,如果多个 case 同时满足条件,Go 会随机选择一个 case 执行,这也称为伪随机选择。这意味着无法确定哪个 case 会被优先选择,因此我们不能依赖于特定 case 的执行顺序。
-
缓冲机制
即使没有读取方,写入方也可以一直往channel里写入,在缓冲区填完之前都不会阻塞。
c := make (chan int, 1024) -
超时机制
channel实现超时机制
time out := make(chan bool, 1)
go func(){
time.sleep(1e9) //1s
timeout <- true
}()
select{
case <-ch:
...
case <-timeout:
...
}
-
channel的传递
channel 本身在定义后也可以通过channel来传递
可以利用这个特性来实现管道特性 -
单向channel
单向channel只能用于发送或者接受数据。只是对channel的一种使用限制。
我们在将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制该函数中可以对channel的操作,比如只能往这个channle写或读。
# 声明
var ch1 chan int
var ch2 chan<-float64 //单向的,只能写float64数据
var ch3 <-chan int //单向的,只能读取int数据
#初始化
ch4 := make(chan int)
ch5 := <-chan int(ch4)
ch6 := chan<- int(ch4)
#用法
func Parse(ch <-chan int){
for value := range ch{
...
}
}
- 关闭channel
close(ch)
判断是否已经被关闭:
x,ok := <-ch
只需要看第二个bool返回值即可,false表示已经被关闭
4.6 多核并行化
可以通过设置环境变量GOAMAXPROCS的值来控制使用多少给CPU核心
或者runtime.GOMAXPROCS()
runtime.NumCOU()获取核心数
4.7 让出时间片
runtime.Gosched()主动让出时间片给其他goroutine
实际上,要想精细控制goroutine行为,须要深入了解runtime包提供的具体功能。
4.8 同步
- 同步锁
sync包提供了两种锁类型:sync.Mutex 和sync.RWMutex。
当一个goroutine获取Mutex后,其他goroutine只能等到该goroutine释放Mutex。
RWMutex是经典的单写多读模型,在读锁占用的情况下会阻止写,但不阻止读。从RWMutex的实现看,其组合了Mutex。读锁调用RLock(),写锁Lock()
任何一个Lock() 或RLock均需要保证对应有Unlock或RUnlock()。
var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock()
}
- 全局唯一操作
对于从全局角度只需要运行一次的代码,比如全局初始化操作,Go提供了一个Once类型来保证全局的唯一性操作。
var once sync.Once
once.Do(func)