接口
【接口的概念】在Go语言中接口(interface)是一种类型,一种抽象的类型
【结构体实现接口方法】
package main import "fmt" type sayer interface { Say() string } type Cat struct{} func (c Cat) Say() string { return "喵喵喵" } func main() { var x sayer // 声明一个接口 c := Cat{} // 初始化一个结构体 x = c // 将结构体赋值给接口 fmt.Println(x.Say()) // 接口就能够调用结构体的方法了 }
【值接收者和指针接收者】
值接收者
package main import "fmt" type Mover interface { move() } type dog struct {} func (d dog) move() { // 值接收者 fmt.Println("狗会动") } func main() { var x Mover // 声明一个接口 var wangcai = dog{} // 旺财是dog类型 x = wangcai // x可以接收dog类型 x.move() var fugui = &dog{} // 富贵是*dog类型 x = fugui // x可以接收*dog类型 x.move() } ---------------------------- 狗会动 狗会动
指针接收者
package main import "fmt" type Mover interface { move() } type dog struct {} func (d *dog) move() { // 指针接收者 fmt.Println("狗会动") } func main() { var x Mover var wangcai = dog{} // 旺财是dog类型 x = wangcai // x不可以接收dog类型 var fugui = &dog{} // 富贵是*dog类型 x = fugui // x可以接收*dog类型 } -------------------------------------- 编译不通过:只能将指针类型赋值给接口,不能把值类型赋值给接口 cannot use wangcai (type dog) as type Mover in assignment: dog does not implement Mover (move method has pointer receiver)
【接口嵌套】
// Sayer 接口 type Sayer interface { say() } // Mover 接口 type Mover interface { move() } // 接口嵌套 type animal interface { Sayer Mover }
以上animal接口等价于
type animal interface { say() move() }
【空接口】
空接口类型的变量可以存储任意类型的变量
package main import "fmt" func main() { // 定义一个空接口x var x interface{} s := "Hello 沙河" x = s fmt.Printf("type:%T value:%v\n", x, x) i := 100 x = i fmt.Printf("type:%T value:%v\n", x, x) b := true x = b fmt.Printf("type:%T value:%v\n", x, x) }
空接口作为函数的参数:使用空接口实现可以接收任意类型的函数参数
// 空接口作为函数参数 func show(a interface{}) { fmt.Printf("type:%T value:%v\n", a, a) }
空接口作为map的值:使用空接口实现可以保存任意值的字典。
// 空接口作为map值 var studentInfo = make(map[string]interface{}) studentInfo["name"] = "沙河娜扎" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo)
【空接口类型断言】:断言就是猜空接口类型
x.(T)
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
func main() { var x interface{} x = "Hello 沙河" v, ok := x.(string) if ok { fmt.Println(v) } else { fmt.Println("类型断言失败") } }
断言方法返回两个参数,v表示x转化为T之后的变量,ok表示猜的对不对,对则为true,错则为false
如果确定断言类型一定是正确的,可直接操作其值:
fmt.println(x.(string))
要进行多次断言则用switch case语句
func justifyType(x interface{}) { switch v := x.(type) { case string: fmt.Printf("x is a string,value is %v\n", v) case int: fmt.Printf("x is a int is %v\n", v) case bool: fmt.Printf("x is a bool is %v\n", v) default: fmt.Println("unsupport type!") } }
包Package
【包的概念】是多个Go源码的集合,是一种高级的代码复用方案。
【包的定义】
一个包可以简单理解为一个存放.go
文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
package 包名
包名可以不和文件夹的名字一样,包名不能包含 - 符号。(但package名称一般都和文件夹名字一样)
包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。
【包的可见性】
如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
【包的导入】要在代码中引用其他包的内容,需要使用import
关键字导入使用的包
// 单行导入 import "包1" import "包2" // 多行导入 import ( "包1" "包2" )
开启了mod之后的包的路径是$GOPATH/pkg/mod开始计算的,使用/进行路径分隔。Go语言中禁止循环导入包。
【自定义包名】
// 单行自定义包名 import m "github.com/Q1mi/studygo/pkg_test" // 多行自定义包名 import ( "fmt" m "github.com/Q1mi/studygo/pkg_test" )
【匿名导入包】如果只希望导入包,而不使用包内部的数据时
import _ "github.com/go-sql-driver/mysql"
【init()函数】
在Go语言程序执行时导入包语句会自动触发包内部init()
函数的调用。需要注意的是: init()
函数没有参数也没有返回值。 init()
函数在程序运行时自动被调用执行,不能在代码中主动调用它
init()函数执行顺序
错误处理
【系统抛出错误Panic】
func test03() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
getCircleArea(-5)
fmt.Println("这里有没有执行")
}
func getCircleArea(radius float32) (area float32) {
if radius < 0 {
// 系统抛出错误
panic("半径不能为负")
}
return 3.14 * radius * radius
}
func main() {
test03()
}
【简单的返回异常】
package main import ( "errors" "fmt" ) func test() error { return errors.New("msg") } func main() { err := test() fmt.Println(err) }
【使用 Errorf 给错误添加更多信息】
package main import ( "fmt" ) func test(msg string) error { return fmt.Errorf("msg:%v", msg) } func main() { err := test("错误") fmt.Println(err) }
【使用错误接口】
写一个结构体,命名约定以 Error 结尾的为错误对象,具体用法如下:
package main import "fmt" type SomeError struct { // 结构体命名必须以 Error 结尾 Reason string } func (s SomeError) Error() string { return s.Reason } func main() { var err error = SomeError{"something happened"} fmt.Println(err) // 输出则执行对象的 Error() 方法 }
因为 Go 语言中不轻易使用异常语句,所以对于所有可能产生Panic的地方都要配合defer recover一下,所有的err都要判断一下。
并发Concurrency
【并发与并行】Go语言的并发通过goroutine
实现
并发:任务数大于cpu的核数,多个任务轮流执行,由于cpu切换速度特别快,看起来像是一起运行,其实是假象。
并行:任务数小于或者等于cpu的核数,那么多个任务是真正意义一起执行
【进程、线程与协程】
进程:
进程是操作系统分配资源的基本单位,进程由操作系统调度。进程可以创建线程,由线程去执行代码。进程之间不共享全局变量,不同进程通过进程间通信来通信。多进程开发稳定性强,进程是重量级的,上下文切换资源开销大
线程:
线程是cpu调度的基本单位,线程的调度是由操作系统负责的,调度算法运行在内核态。 线程里面可以包含多个协程。线程间通过共享内存通信。上下文切换很快,资源开销较少
协程:
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。一个进程内部可以运行多个线程,而每个线程又可以运行很多协程。线程要负责对协程进行调度,保证每个协程都有机会得到执行。当一个协程睡眠时,它要将线程的运行权让给其它的协程来运行,而不能持续霸占这个线程。同一个线程内部最多只会有一个协程正在运行。
协程的三个状态:
协程可以简化为三个状态,运行态、就绪态和休眠态。同一个线程中最多只会存在一个处于运行态的协程,就绪态的协程是指那些具备了运行能力但是还没有得到运行机会的协程,它们随时会被调度到运行态,休眠态的协程还不具备运行能力,它们是在等待某些条件的发生,比如 IO 操作的完成、睡眠时间的结束等。
操作系统对线程的调度是抢占式的,也就是说单个线程的死循环不会影响其它线程的执行,每个线程的连续运行受到时间片的限制。
Go 语言运行时对协程的调度并不是抢占式的。如果单个协程通过死循环霸占了线程的执行权,那这个线程就没有机会去运行其它协程了,你可以说这个线程假死了。不过一个进程内部往往有多个线程,假死了一个线程没事,全部假死了才会导致整个进程卡死。
每个线程都会包含多个就绪态的协程形成了一个就绪队列,如果这个线程因为某个别协程死循环导致假死,那这个队列上所有的就绪态协程是不是就没有机会得到运行了呢?Go 语言运行时调度器采用了 work-stealing 算法,当某个线程空闲时,也就是该线程上所有的协程都在休眠(或者一个协程都没有),它就会去其它线程的就绪队列上去偷一些协程来运行。也就是说这些线程会主动找活干,在正常情况下,运行时会尽量平均分配工作任务。
【goroutine】
Goroutine概念:goroutine可以认为是轻量级的线程,goroutine是由Go语言的运行时(runtime)调度完成。
Goroutine优势:与创建线程相比,创建成本和开销都很小,每个goroutine的堆栈只有几kb
主Goroutine:main函数就是一个主goroutine,其他所有的goroutine都平级。主goroutine退出则程序退出
设置线程数:默认情况下,Go 运行时会将线程数会被设置为机器 CPU 逻辑核心数,比如
物理cpu数:主板上实际插入的cpu数量,可以数不重复的 physical id 有几个(physical id)
cpu核数:单块CPU上面能处理数据的芯片组的数量,如双核、四核等 (cpu cores)
逻辑cpu数:一般情况下,逻辑cpu=物理CPU个数×每颗核数,如果不相等的话,则表示服务器的CPU支持超线程技术(HT:简单来说,它可使处理器中的1 颗内核如2 颗内核那样在操作系统中发挥作用。这样一来,操作系统可使用的执行资源扩大了一倍,大幅提高了系统的整体性能,超线程技术下:此时逻辑cpu=物理CPU个数×每颗核数x2)
【channel】
channel概念:不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列数据交流定制了特殊的语法 —— 通道。
协程之间用通道来进行通信
声明channel:
// 缓冲型通道,里面只能放整数 var bufferedChannel = make(chan int, 1024) // 非缓冲型通道 var unbufferedChannel = make(chan int)
channel的读写:
Go 语言为通道的读写设计了特殊的箭头语法糖 <-,让我们使用通道时非常方便。把箭头写在通道变量的右边就是写通道,把箭头写在通道的左边就是读通道。一次只能读写一个元素。
package main import "fmt" func main() { var ch chan int = make(chan int, 4) for i:=0; i<cap(ch); i++ { ch <- i // 写通道 } for len(ch) > 0 { var value int = <- ch // 读通道 fmt.Println(value) } }
channel的读写堵塞:
通道满了,写操作就会阻塞,协程就会进入休眠,直到有其它协程读通道挪出了空间,协程才会被唤醒。如果有多个协程的写操作都阻塞了,一个读操作只会唤醒一个协程。
通道空了,读操作就会阻塞,协程也会进入睡眠,直到有其它协程写通道装进了数据才会被唤醒。如果有多个协程的读操作阻塞了,一个写操作也只会唤醒一个协程。
channel的关闭
package main import "fmt" func main() { var ch = make(chan int, 4) ch <- 1 ch <- 2 close(ch) value := <- ch fmt.Println(value) value = <- ch fmt.Println(value) value = <- ch fmt.Println(value) } ------- 1 2 0
通道如果没有显式关闭,当它不再被程序使用的时候,会自动关闭被垃圾回收掉。不过优雅的程序应该将通道看成资源,显式关闭每个不再使用的资源是一种良好的习惯。