Golang学习笔记(下)
前篇:Golang学习笔记(上)
十四、错误处理
14.1使用error类型
func New(text string) error
例子:
package main
import (
"errors" // 导入errors包
"fmt"
)
func main() {
var number, divisor int
fmt.Println("请输入一个整数作为分子:")
fmt.Scan(&number)
fmt.Println("请输入一个整数作分母:")
fmt.Scan(&divisor)
result, err := divide(number, divisor)
fmt.Println("result", result)
if err != nil {
fmt.Println(err)
}
}
// 除法函数,参数number是分子,参数divisor是分母
func divide(number, divisor int) (float32, error) {
if divisor == 0 {
// 返回多值数据
return 0.0, errors.New("分母不能为0")
}
return float32(number / divisor), nil
}
14.2错误信息格式化
使用fmt包中的Errorf函数
func divide(number, divisor int) (float32, error) {
if divisor == 0 {
// 返回多值数据
error := fmt.Errorf("您输入的分母是:%d", divisor)
return 0.0, error
}
return float32(number / divisor), nil
}
14.3自定义错误类型
// 自定义错误类型
type error interface {
Error() string
}
举例
package main
import (
"fmt"
)
// 自定义错误类型结构体
type DivisionByZero struct {
// 错误信息
message string
}
// 实现error接口的Error()方法
func (d DivisionByZero) Error() string {
return d.message
}
// 除法函数,参数number是分子,参数divisor是分母
func divide(number, divisor int) (float32, error) {
if divisor == 0 {
// 创建一个DivisionByZero错误实例,并返回
return 0.0, DivisionByZero{message: "除数不能为0"}
}
return float32(number) / float32(divisor), nil
}
func main() {
var number, divisor int
fmt.Println("请输入一个整数作为分子:")
fmt.Scan(&number)
fmt.Println("请输入一个整数作分母:")
fmt.Scan(&divisor)
result, err := divide(number, divisor)
if err != nil {
// 打印具体的错误信息
fmt.Println("发生错误:", err.Error())
return
}
fmt.Printf("result: %f\n", result)
}
14.4错误机制
-
go语言提供了panic()和recover()函数
-
defer关键字实现错误处理
延迟执行
defer语句用来在函数最终要返回前被执行,用来释放资源,在错误处理的过程中,一个非常重要的环节就资源释放,无论执行成功,还是执行错误,都应该保证释放这些资源,此时可以使用defer关键字延迟执行,保证在程序运行完成之前释放资源。
当有多个defer语句时,defer语句遵守后进先出
原则。
进入宕机状态
用于触发一个运行时错误。
-
自动进入当即状态,当发生运行期错误时自动进入。
-
手动触发,程序员可以根据自己的需要通过panic()函数可以手动触发宕机状态。
package main
import "fmt"
func main() {
intSclical := []int{1, 2, 3}
// 下标月结引发的宕机
fmt.Println(intSclical[3])
}
输出:
D:Go/bin/go.exe run panic.go [F:/Code/Go/src]
panic: runtime error: index out of range [3] with length 3
goroutine 1 [running]:
main.main()
F:/Code/Go/src/panic.go:8 +0x15
exit status 2
错误: 进程退出代码 1.
手动宕机
package main
import "fmt"
func main() {
intSclical := []int{1, 2, 3}
// 下标超出索引范围,自动引发的宕机
// fmt.Println(intSclical[3])
var index int = -1
fmt.Println("请输入下标索引: ")
fmt.Scan(&index)
if index > 3 {
panic("索引超出范围")
} else {
fmt.Println("输出元素为:", intSclical[index])
}
}
14.5从宕机状态恢复
使用recover()函数从宕机状态恢复
package main
import (
"fmt"
)
// 除法函数,参数number是分子,参数divisor是分母
func divide(number, divisor int) (float32, bool) {
if divisor == 0 {
// 触发panic,表示发生了严重的错误
panic("除数不能为0")
}
return float32(number) / float32(divisor), true
}
func main() {
var number, divisor int
fmt.Println("请输入一个整数作为分子:")
fmt.Scan(&number)
fmt.Println("请输入一个整数作分母:")
fmt.Scan(&divisor)
// 使用defer和recover来捕获panic
defer func() {
if r := recover(); r != nil {
fmt.Println("程序遇到严重错误:", r)
}
}()
result, ok := divide(number, divisor)
if !ok {
// 实际上,由于我们使用了panic,这里的ok检查是多余的
// 因为panic会导致程序停止执行,不会到达这里
fmt.Println("发生错误,无法执行除法")
return
}
fmt.Printf("result: %f\n", result)
}
十五、并发编程
15.1进程
-
一个进程就是一个执行中的程序
-
每一个进程都有自己独立的一块内存空间
-
每一个进程都有一组系统资源
15.2线程
-
线程和进程都是一段完成特定功能的代码。
-
它们都是程序中的单个顺序控制流程
-
多个线程共享一块内存空间和一组系统资源。
-
在切换不同线程之间时,系统的开销比切换进程要小得多,称为轻量级进程。
-
一个进程可以包含多个线程。
15.3协程
-
协程(Goroutines)是一种轻量级的线程。
-
不阻塞线程,但可以挂起的计算过程。
-
挂起几乎没有开销。
-
底层库处理一部阻塞任务。
-
协程代码流程顺序,无需大量的回调函数。
-
类似同步代码,已于理解、调试和开发。
15.4创建协程
-
go语言的并发编程时通过协程(Goroutines)实现的。
-
在Go中创建协程很简单,只要在函数或方法前加
go关键字
,则会创建一个新协程。
创建协程示例
package main
import "fmt"
func display(str string) {
fmt.Println(str)
}
func main() {
// 创建协程,异步调用函数
go display("welcome to Beijing!")
// 正常调用函数
display("Hello World")
}
每次运行的结果不一致,原因
也就是说主线程会调用display(“Hello World!”)和协程中的dispaly(“Welcome to Beijing!”),这两调用的过程时随机的,谁先谁后不一定。
两个协程执行是异步的,哪个先执行,哪个后执行,都是不确定的,中就是并发程序的特点。
15.5通道
声明通道
-
使用chan关键字声明,语法如下:
var Channel_name chan Type
-
使用make()函数创建通道,语法如下:
channel_name := make(chan Type)
举例
package main
import "fmt"
func main() {
// 1.使用chan声明通道
var mychannel1 chan int
fmt.Printf("mychannel1:%V\n", mychannel1)
fmt.Printf("mychannel1类型:%T\n", mychannel1)
// 2.通过make()函数创建通道
mychannel2 := make(chan int)
fmt.Printf("mychannel2:%v\n", mychannel2)
fmt.Printf("mychannel类型:%T\n", mychannel2)
}
发送和接收数据
-
向通道发生数据语法如下:
mychannel<-data
-
从通道接收数据语法如下:
接收数据变量:=<-mychannel
关闭通道
使用close()函数关闭通道。
示例代码:
package main
import "fmt"
func display(chstr chan string) {
// 向通道发生数据
chstr <- "Hello,World!"
}
func main() {
// 1.使用chan声明通道
var mychannel1 chan int
fmt.Printf("mychannel1:%V\n", mychannel1)
fmt.Printf("mychannel1类型:%T\n", mychannel1)
// 2.通过make()函数创建通道
mychannel2 := make(chan int)
fmt.Printf("mychannel2:%v\n", mychannel2)
fmt.Printf("mychannel类型:%T\n", mychannel2)
// 发生和接收数据
message := make(chan string)
// 启动协程
go display(message)
// 从通道中接收数据
msg := <-message
fmt.Println(msg)
// 关闭通道
ch := make(chan int, 3)
ch <- 2
ch <- 3
close(ch)
fmt.Println("关闭通道")
// 试图向关闭的通道发生数据(error)
// ch <- 4
fmt.Println("关闭通道")
// 试图从关闭的通道中接收数据
msg1 := <-ch
fmt.Println("从通道中接收数据")
mgs2, ok := <-ch
fmt.Println("从通道中接收的数据:", msg)
fmt.Println(ok)
fmt.Println(msg1, mgs2)
}
遍历通道
package main
import "fmt"
// 遍历通道
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
}
func main() {
// 创建int类型通道
ch := make(chan int)
// 创建协程
go producer(ch)
// 遍历从通道中接收的数据
for v := range ch {
fmt.Println("接受:", v)
}
}
单向通道和双向通道
channel_name := make(<-chan Typr) // 只接收数据
channel_name := make(chan<- Typr) // 只发送数据
package main
import "fmt"
func main() {
// 创建只接收的数据通道
mychannel1 := make(<-chan string)
// 创建只发送的数据通道
mychannel2 := make(chan<- string)
fmt.Printf("mychannel1:%v\n", mychannel1)
fmt.Printf("mychannel1类型:%T\n", mychannel1)
fmt.Printf(<-mychannel2) // error:invalid operation: cannot receive from send-only channel mychannel2 (variable of type chan<- string)
fmt.Printf("mychannel2:%v\n", mychannel2)
fmt.Printf("mychannel2类型:%T\n", mychannel2)
}
缓冲区通道
- 无缓冲区通道
无缓冲区通道,通道中无法存储数据。发送和接收数据时是同步的,无缓冲区通道使用make()函数创建,自谦创建的都是无缓冲区通道。
有两种情况下会造成程序的阻塞:
-
当一个协程A将数据发送给通道时,其他协程还未接收数据时,协程A被迫阻塞,知道其他协程接收数据后,协程A才能继续执行。
-
当一个协程A试图从通道接收数据时,如果通道A中没有数据,则会等待其他协程发送,知道其他协程发送数据后,协程A才能继续执行。
- 有缓冲区通道
有缓冲区通道,通道中可以存储数据,发送和接受时是异步的,创建有缓冲区的通道可以使用make(chan Type int)函数,
该函数的第一个参数是通道中的数据类型,第二个参数是设置缓冲区的大小。
15.6使用select语句
语法:
select {
case 通道1:
语句组1
case 通道2:
语句组2
...
case 通道n:
语句组n
default:
语句组n+1
}
举例:
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(5 * time.Second)
ch <- "Hello World."
}
func main() {
// 声明两个通道c1和c2
c1 := make(chan string)
c2 := make(chan string)
// 协程处理匿名函数
go func() {
// 休眠1秒
time.Sleep(1 * time.Second)
c1 <- "one"
}()
// 协程处理匿名函数
go func() {
// 休眠2秒
time.Sleep(2 * time.Second)
// 发送数据
c2 <- "two"
}()
// 循环遍历从通道中接收的数据
for i := 0; i < 2; i++ {
select {
// 从c1通道接收数据
case msg1 := <-c1:
fmt.Println("received", msg1)
// 从c2通道接收数据
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
// default
ch := make(chan string)
// 启动协程
go process(ch)
// 一直遍历从通道中取数据
for {
time.Sleep(1 * time.Second)
select {
case v := <-ch:
fmt.Println("接收数据:", v)
return
default:
fmt.Println("没有数据接收。")
}
}
}
for {
time.Sleep(1 * time.Second)
select {
case v := <-ch:
fmt.Println("接收数据:", v)
return
default:
fmt.Println("没有数据接收。")
}
}
}