本篇概要:
8. Mysql 调用;
8.1 集成 Mysql 驱动、调用数据库、查询数据、for 循环;
安装 mysql 驱动
go get -u github.com/go-sql-driver/mysql
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:asdf@tcp(localhost:3306)/test?charset=utf8")
if err != nil {
fmt.Println("连接错误" + err.Error())
return
}
rows, err := db.Query("SELECT u_id,u_name FROM webusers")
if err != nil {
fmt.Println("查询错误" + err.Error())
return
}
// 简单取值
for rows.Next() {
var uid int
var uname string
rows.Scan(&uid, &uname)
fmt.Println(uid, uname)
}
fmt.Println(rows.Columns())
// 结构体输出
userModel := models.UserModel{}
for rows.Next() {
rows.Scan(&userModel.Uid, &userModel.Uname) // 修改原有的值
fmt.Println(userModel)
}
fmt.Println(userModel)
// 输出:
// {1 张三}
// {2 大胖胖}
// {3 李四}
// {3 李四}
}
8.2 入门 slice 切片、查询返回实体集合;
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
arr:=[5]int{1,2,3} // 定义好数组长度,没赋值会补0(数组一旦定义好长度后,是不能改变的)
fmt.Println(arr)
fmt.Println(len(arr),cap(arr)) // len 求长度,cap求容量
// 返回
// [1 2 3 0 0]
// 5 5
}
slice 切片
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
s := []int{1,2,3} // 定义切片,没有定义长度
fmt.Println(s)
fmt.Println(len(s),cap(s))
// 返回 [1 2 3] 3 3
// var ss []int // ss 的 len 和 cap 都是 0
// 接下来给slice分片初始化 :分配空间和初始化长度
// ss = make([]int, 5) // 内置的 make 函数对切片初始化
// 以上的简化写法
ss := make([]int, 5) // [0 0 0 0 0] 5 5
fmt.Println(ss)
fmt.Println(len(ss),cap(ss))
// 带值初始化
arr := [5]int{1,2,3,4,5} // 有长度就是 arr,没长度就是 slice
sss := arr[0:4] // 构建切片,区间 [4,10),为 >=4,<10;s:=arr[0:] 或 s:=arr[:5] 也是一样
fmt.Println(sss) // [1 2 3 4]
fmt.Println(len(sss),cap(sss)) // 4 5
// 增加内容:以下写法错误 sss[5] = 6,而是使用 append 函数添加数据
// 如果 cap 不够则会创建一个新的 slice,把原有的 slice 拷贝进去。
// 因此实际开发要预估一个合适的 cap,否则会导致性能开销
arr2 := [5]int{1,2,3,4,5}
ssss := arr2[:2] // [1 2 ] 2 5
// ssss =append(ssss, 3) // [1 2 3] 3 5
// ssss =append(ssss, 3,4,5) // [1 2 3 4 5] 5 5
ssss =append(ssss, 3,4,5,6) // [1 2 3 4 5 6] 6 10
fmt.Println(ssss)
fmt.Println(len(ssss),cap(ssss))
}
- 文件
/Users/go/src/com.test/appmain/main.go
,查询返回实体集合
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"com.test/models"
)
func main() {
db, err := sql.Open("mysql", "root:asdf@tcp(localhost:3306)/test?charset=utf8")
if err != nil {
fmt.Println("连接错误" + err.Error())
return
}
rows, err := db.Query("SELECT u_id,u_name FROM webusers limit 0,10 ")
if err != nil {
fmt.Println("查询错误" + err.Error())
return
}
// 切片使用
userModels:=[]models.UserModel{}
for rows.Next(){
temp:=models.UserModel{}
rows.Scan(&temp.Uid,&temp.Uname)
userModels=append(userModels,temp) // 往切片添加数据
}
fmt.Println(userModels)
// [{1 张三} {2 大胖胖} {3 李四} {4 大长脸} {5 小朱} {6 小狗} {7 刘九}]
}
8.3 查询返回通用数组、空接口使用、类型断言、range;
- 之前从用户表里取数据,返回用户模型(userModels切片)。但是这个太“针对性”,假设要做一个数据库工具类,不可能都返回指定的 Model
- 空接口概念
type all interface{
}
// 任何类型都可以是空接口(因为它里面什么都“没有”)
var i all = models.UserModel{}
var i all = 123
var i all = "abc"
// 匿名空接口,数据库取值就不需要说明啥类型
var i interface{} = "123"
// 如果不给它赋值,它里面的每一项不会初始化为 0 或者 “”,而是一个 nil
// 尝试碎片
var list=make([]all,2)
fmt.Println(list)
// 事实上,都没有必要去定义一个all结构,可以使用以下方式:
list:=make([]interface{},2)
list[0] = 1
list[1] = "abc"
fmt.Println(list)
// 字节数组转 string
b:=[]byte{97,98,99}
fmt.Println(string(b)) // 直接这样即可:abc
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
_ "com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, _ := sql.Open("mysql", "root:asdf@tcp(localhost:3306)/test?charset=utf8")
rows, _ := db.Query("SELECT u_id,u_name FROM webusers limit 0,10 ")
// 创建空接口(匿名)切片,长度不知道就写 0
allRows := make([]interface{}, 0)
//for rows.Next() {
// var uid int
// var uname string
// rows.Scan(&uid, &uname)
//
// // 方法 1(查询列多,代码就太啰嗦
// //oneRow := make([]interface{}, 2)
// //oneRow[0] = uid
// //oneRow[1] = uname
// // allRows = append(allRows, oneRow)
//
// // 方法2:空接口,切片的方式塞入
// allRows = append(allRows, []interface{}{uid, uname})
//}
// 方法3:不要定义变量
for rows.Next() {
// 通过接口出来的数据默认是字节数组切片
oneRow := make([]interface{}, 2)
rows.Scan(&oneRow[0], &oneRow[1])
// 遍历出字节
//for i:=0; i<len(oneRow); i++{
// fmt.Println(i,oneRow[i])
//}
// 遍历
// range 直接返回数组或切片的 index 以及每一项的 val
for i,val := range oneRow {
// fmt.Println(i, val)
// 类型断言
v, ok := val.([]byte)
if(ok){
//fmt.Println(string(v))
oneRow[i] = string(v) // 每一行塞入大切片
}
}
allRows = append(allRows, oneRow)
}
// [[1 张三] [2 大胖胖] [3 李四] [4 大长脸] [5 小朱] [6 小狗] [7 刘九]] 外部大切片,里面小数组
fmt.Println(allRows)
}
8.4 查询返回通用map(字典)、可变参数用法;
概括:
- 之前的取值并不方便,得各种遍历,如果要单独取其中一列就得根据索引
- 调用的不知道要取的列,索引是什么,是否继续遍历
- java 的 hashmap 或者 python 的dic 等,很多语言存在着一种“以键值对方式存在的数据类型”
- go 里面就是 map 类型
// 注意它的参数顺序是:
map[KeyType]ValueType // []里面放的是key的类型
// 用 make 的方式(和定义切片类似)
m := make(map[string]string)
m["username"] = "hua"
m["userqq"] = "123456"
// 带值初始化
m := map[string]string{"username":"abc"}
m := map[string]string{"username":"abc","userphone":"12345678"}
m["sex"] = "男"
// 删除
delete(m, sex)
// 对于 valuetype 假设是不固定类型
m := map[string]interface{}{"username":"abc","userid":123}
m["sex"] = "男"
fmt.Println(m)
// 关于遍历
m := map[string]interface{}{"username":"abc","userid":123}
m["sex"] = "男"
for key,value := range m{
fmt.Println(key, value)
}
// 关于不定参数
// 不知道传几个参数就这么写,三个点写在类型旁边
func test(str ...string){
for _,v := range str {
fmt.Println(v)
}
}
// 调用
test("aa","bb","cc")
list := []string{"aa", "bb", "cc"}
test(list...)
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
_ "com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, _ := sql.Open("mysql", "root:asdf@tcp(localhost:3306)/test?charset=utf8")
rows, _ := db.Query("SELECT u_id,u_name FROM webusers limit 0,10 ")
// 获取sql取值后的列名(字段名)
columns, _:=rows.Columns()
// 创建空接口(匿名)切片,长度不知道就写 0
allRows := make([]interface{}, 0)
for rows.Next() {
// 定义 map
// fieldMap := make(map[string]interface{}) // 空接口
fieldMap := make(map[string]string)
// 通过接口出来的数据默认是字节数组切片
oneRow := make([]interface{}, len(columns)) // 定义一行切片
scanRow := make([]interface{}, len(columns))
for i, _ := range oneRow {
scanRow[i] = &oneRow[i] // 传的是地址
}
// rows.Scan(&oneRow[0], &oneRow[1])
rows.Scan(scanRow...)
// 遍历
// range 直接返回数组或切片的 index 以及每一项的 val
for i,val := range oneRow {
// 类型断言
v, ok := val.([]byte)
if(ok){
// oneRow[i] = string(v)
fieldMap[columns[i]] = string(v)
}
}
allRows = append(allRows, fieldMap)
}
// [map[u_id:1 u_name:张三] map[u_id:2 u_name:大胖胖] map[u_id:3 u_name:李四] map[u_id:4 u_name:大长脸] map[u_id:5 u_name:小朱] map[u_id:6 u_name:小狗] map[u_id:7 u_name:刘九]]
fmt.Println(allRows)
}
9. Go 协程入门;
9.1 认识协程、启动最简单的协程;
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
_ "github.com/go-sql-driver/mysql"
"fmt"
"time"
)
func sum(max int) {
result:=0
for i:=1;i<=max;i++{
result=result+i
}
time.Sleep(time.Second*2)
fmt.Println(result)
}
func main() {
// 启动协程,关键字 go
// 但是不会输出结果,因为当 main() 函数执行完后,协程还没执行完
go sum(100)
// 不正规方法,使用 time 包的 sleep 功能,进行休眠
// 无法“完美的”解决协程的执行顺序问题
time.Sleep(time.Second*3)
}
9.2 协程通信,channel、死锁、多协程速度比较;
channel 类型
-
go 里面的核心数据类型,有了它我们可以方便的进行协程间数据通信。其原理来源于 CSP 模型理论,go实现了部分理论
-
CSP 模型由并发执行的实体(如进程或线程)组成,实体之间通过发送消息进行通信,其中 channel 承担了了实体和实体之间发送消息的通道
-
在 go 里面 goroutine 就是实体,它里面也有个 channel 来完成通信
-
文件
/Users/go/src/com.test/appmain/main.go
package main
import (
_ "github.com/go-sql-driver/mysql"
"fmt"
"time"
)
func sum(max int, c chan int) {
result:=0
for i:=1;i<=max;i++{
result=result+i
}
// 把 result 发送给 channel (数据写入通道)
c <- result
}
func main() {
// 使用 go 关键字运行协程 是不能直接把返回数据给 return 出来的,需要使用到 channel
// 初始化channel, 没有任何容量参数
// c := make(chan int, 2) 带有容量 =2 的 channel
c := make(chan int)
go sum(100, c) // 传递就必须接收,否则会出现死锁
ret := <- c // 读取数据 ,接收者
fmt.Println(ret)
}
上面的代码中
- 对于发送者:如果没有接收者读取 channel (<- channel),则发送者 (channel <-) 会一直阻塞。
- 同样对于接收者: 接收操作是阻塞的直到发送者发送数据
- 根据上面的特性,就能实现当goroutine执行完成后 得到数据
- 死锁代码(错误):
sum(100,c) //这前面没有 go 关键字,同一个线程进行(主线程),c <- result 发送,没有 go 就没法接收,卡死
ret := <-c //读取数据 ,接受者(进行不到这一步)
fmt.Println(ret)
带缓冲的 channel
- 刚才我们创建的是:c:=make(chan int)
- 注意,其实还有参数:c:=make(chan int, 2)
- 这好比创建一个 带有容量 =2 的 channel (好比队列),只有当队列塞满时发送者会阻塞,队列空时接受者会阻塞
- 如果队列什么都没塞,也会阻塞(死锁)
改造案例(多协程)
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"time"
)
func sum(min,max int,c chan int) {
result:=0
for i:=min;i<=max;i++{
result=result+i
}
c <- result
}
func main() {
num := 100000000
start:=time.Now() // 开始时间
c:=make(chan int,2) // 创建两个协程
go sum(1,num/2,c);
go sum(num/2+1,num,c)
ret1,ret2:=<- c,<- c // ret1 和 ret2 分别读取
end:=time.Now() // 结束时间
fmt.Println(end.Sub(start),ret1+ret2)
}
9.3 协程 channel 交叉合并数组、遍历channel;
一些例子:
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
_ "github.com/go-sql-driver/mysql"
"strings"
"fmt"
"time"
)
func main() {
users := strings.Split("shenyi,zhangsan,lisi,wangwu",",") // 拆分字符串为数组
ages := strings.Split("19,21,25,26",",")
alllist := make([]string, 0) // 切片
//alllist := []string{} // 同样切片写法
// 组合数组
//alllist = append(alllist, users...)
//alllist = append(alllist, ages...)
// 遍历
for i, v := range users {
alllist = append(alllist, v)
alllist = append(alllist, ages[i])
}
// 遍历返回:[shenyi 19 zhangsan 21 lisi 25 wangwu 26]
fmt.Println(alllist)
}
利用协程和channel来完成
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
_ "github.com/go-sql-driver/mysql"
"strings"
"fmt"
"time"
)
func main() {
users := strings.Split("shenyi,zhangsan,lisi,wangwu",",") // 拆分字符串为数组
ages := strings.Split("19,21,25,26",",")
c1 := make(chan string, len(users)) // 创建管道,定义长度(有缓冲)
c2 := make(chan string, len(ages))
// 匿名函数,无需写在外部
go func() {
for _ , v := range users { // i 用不到 改为"_"
c1 <-v
}
close(c1)
}()
go func() {
for _ , v := range ages {
c2 <-v
}
close(c2)
}()
// channel 也可以通过 range 遍历,但是它会一只遍历,直到关闭
// 所以需要用到 close 函数(关闭的 channel 取值不会阻塞,返回零值)
for v := range c1 {
fmt.Println(v)
}
for v := range c2 {
fmt.Println(v)
}
}
更新(可能无法运行完成):
package main
import (
_ "github.com/go-sql-driver/mysql"
"strings"
"fmt"
"time"
)
func main() {
users := strings.Split("shenyi,zhangsan,lisi,wangwu",",") // 拆分字符串为数组
ages := strings.Split("19,21,25,26",",")
c1, c2 := make(chan bool), make(chan bool)
ret := make([]string, 0)
// 同时开启两进程
go func() {
for _ , v := range users {
<-c1 // 只是吐出来
ret = append(ret, v)
c2<-true // 执行到这一步 下面的 c2 就不会被阻塞了
}
}()
go func() {
for _ , v := range ages {
<-c2
ret = append(ret, v)
c1<-true //执行到这一步 上面的 c1 就不会被阻塞了
}
}()
c1<-true // 把值发送给 c1
fmt.Println(ret)
// 可能输出不完整:[shenyi 19 zhangsan 21]
// [shenyi 19 zhangsan 21 lisi 25 wangwu 26]
}
9.4 多协程抓取网页、ioutil简单读写、格式化字符串;
ioutil包:go 里的 IO 操作相关封装,实现了 IO 操作的常用函数
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"net/http"
)
func main() {
// 创建文件
content := "hello go"
ioutil.WriteFile("src/com.test/files/test.txt", []byte(content),777)
// 读取文件
getFile,_:=ioutil.ReadFile("src/com.test/files/test.txt")
fmt.Println(string(getFile))
// 读取网络 url
url:="https://news.cnblogs.com/n/page/%d/" // %d 占位符
for i:=1; i<=3; i++ {
url :=fmt.Sprintf(url, i)
res, _ := http.Get(url)
cnt, _ := ioutil.ReadAll(res.Body)
ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", i), cnt, 777)
}
}
- 文件
/Users/go/src/com.test/appmain/main.go
,使用协程
package main
import (
"com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"net/http"
)
func main() {
// 读取网络 url
url:="https://news.cnblogs.com/n/page/%d/" // %d 占位符
// 协程执行的结果不是按顺序完成,用map
c := make(chan map[int][]byte)
for i:=1; i<=3; i++ {
go func(index int) {
url :=fmt.Sprintf(url, index) //格式化字符串,生成每一页的页码url
res, _ := http.Get(url) //利用http库 读取网页内容,返回response对象
cnt, _ := ioutil.ReadAll(res.Body) //读出response的body部分
c <- map[int][]byte{index:cnt} //把结果构建成一个map 插入到 channel
if index == 3 {
close(c)
}
}(i)
}
for getcnt := range c{ // 主线程干的事,不断的 range channel,直至 channel 被关闭
for k, v := range getcnt {
// 把内容写入到文件中
ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", k), v, 777)
}
}
}
9.5 抓取网页出现异常处理、panic、defer;
Golang 中的异常
// 很多语言都有对应的处理,最知名的就是try catch finally,利用 throw 关键字来抛出异常
// 使用 panic 关键字,可以抛出异常 ,抛出异常后,函数运行即中止
func test() {
a:=1
panic("my exp")
a++
fmt.Println(a)
}
// 如何抛出异常后下面的a依然能打印出来
// 要用 defer 定义。 这很类似 finally、也好比是析构函数。
// defer 函数可以指定多个,*后进先出,延迟执行,常用于销毁资源、释放句柄等
func test() {
a:=1
defer func() {
a++
fmt.Println(a)
}() //注意定义顺序
panic("my exp")
}
// 运行顺序
// ret 被初始成 0 值,return a 相当于把 a 的值1 复制给 ret 然后 ret++ 就是 2
func test() (ret int) {
a := 1
defer func() {
ret ++
}()
return a
}
// main() 函数捕获异常
func test() (ret int) {
defer func() {
ret = -1
}()
panic("exp")
}
func main() {
defer func() {
err := recover() // 获取 exp
if err != nil {
fmt.Println(err)
}
}()
fmt.Println(test()) // 没有被执行
}
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"net/http"
)
func test() {
panic("exp")
}
func main() {
// 读取网络 url
url:="https://news.cnblogs.com/n/page/%d/" // %d 占位符
// 协程执行的结果不是按顺序完成,用map
c := make(chan map[int][]byte)
for i:=1; i<=3; i++ {
go func(index int) {
defer func() {
if err:=recover();err!=nil{
fmt.Println(err) // 异常打印
}
if index==3{
close(c)
}
}()
url :=fmt.Sprintf(url, index) //格式化字符串,生成每一页的页码url
res, _ := http.Get(url) //利用http库 读取网页内容,返回response对象
cnt, _ := ioutil.ReadAll(res.Body) //读出response的body部分
if index==3{
test()
}
c <- map[int][]byte{index:cnt} //把结果构建成一个map 插入到 channel
}(i)
}
for getcnt := range c{ // 主线程干的事,不断的 range channel,直至 channel 被关闭
for k, v := range getcnt {
// 把内容写入到文件中
ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", k), v, 777)
}
}
}
// 运气好,三页都能抓下来。运气不好,只能抓 1-2 页
// 创建的协程并不是按顺序执行的
//i := 0
//for getcnt := range c{ // 主线程干的事,不断的 range channel,直至 channel 被关闭
// i++
// for k, v := range getcnt {
// // 把内容写入到文件中
// ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", k), v, 777)
// }
// if i==3 {
// close(c)
// }
//}
9.6 select、超时处理、goto语句;
关键词 select:select 很类似 switch case ,只不过用于channel通信(要么send要么receive)
select {
case i<-c:
xxoo
case c<-123:
xxoo
default:
}
- 按顺序判断,如果只有一个 case 通过,则执行该 case
- 如果多个 case 都通过,则随机选一个执行
- 如果多个 case 都通过,则随机选一个执行
result := map[int][]byte{}
for {
select {
case result =<- c:
for k, v := range result {
// 把内容写入到文件中
ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", k), v, 777)
}
}
}
关键词 time 包
- time.After(time.Second*3)
- 等待指定时间后,向返回的 channel 里面写入当前时间。此函数不阻塞,它的返回值是一个只读 channel
- 定义只读的channel: c := make (<-chan int)
- 定义只写的channel:c := make (chan<- int)
func main() {
cc:=time.After(time.Second*3)
fmt.Println("不阻塞")
fmt.Println(<-cc) // 3秒后 出现结果
// 不阻塞
// 2022-04-21 17:42:12.356731 +0800 CST m=+3.004456563
}
label(标签)语法
i:=0
a:fmt.Println(i)
time.Sleep(time.Second*2)
i++
goto a
// 一个很简单的死循环代码,for都不需要(不过不是很建议使用,代码会比较乱)
goto a
a:fmt.Println(i) //可以定义在goto下面,此时就没有了循环效果
// 同样的,break标签依然可以实现跳出循环
// 没有 for 的死循环
func main() {
iii :=0
abc:fmt.Println(iii)
goto abc
}
- 文件
/Users/go/src/com.test/appmain/main.go
package main
import (
"com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"net/http"
"time"
)
func main() {
// 读取网络 url
url:="https://news.cnblogs.com/n/page/%d/" // %d 占位符
// 协程执行的结果不是按顺序完成,用map
c := make(chan map[int][]byte)
for i:=1; i<=10; i++ {
go func(index int) {
defer func() {
if err:=recover();err!=nil{
fmt.Println(err) // 异常打印
}
}()
url :=fmt.Sprintf(url, index) //格式化字符串,生成每一页的页码url
res, _ := http.Get(url) //利用http库 读取网页内容,返回response对象
cnt, _ := ioutil.ReadAll(res.Body) //读出response的body部分
c <- map[int][]byte{index:cnt} //把结果构建成一个map 插入到 channel
}(i)
}
// 也并不是最好的解决方案
result := map[int][]byte{}
myloop:for {
select {
case result =<-c: // 赋值变量
for k, v := range result {
// 把内容写入到文件中
ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", k), v, 777)
}
case <-time.After(time.Second*3): // 只读
break myloop // 跳出标签
}
}
}
9.7 sync包入门、等待协程执行结束;
把生成文件放到协程里执行,使用 sync 包
- sync 包有个很有用的功能 WaitGroup,Add(参数 int)、done()、wait()
内部创建一个计数器,使用
- Add 用来添加 goroutine 的个数
- Done 执行一次数量减 1, 好比Add(-1)
- Wait 用来阻塞主线程,直到所有协程执行完毕 (好比其他语言中的join())
var wg sync.WaitGroup
// wg.Add(5) // 就不会执行 全部完成
for i:=0;i<=5;i++ {
go func(index int ) {
defer func() {
wg.Done() // 对内部的计数器 -1 相当于 wg.Add(-1)
}()
time.Sleep(time.Second*1)
fmt.Println(index,"执行完毕")
}(i)
wg.Add(1)
}
fmt.Println("任务总数:", runtime.NumGoroutine())
wg.Wait()
fmt.Println("全部完成")
// 1 执行完毕
//4 执行完毕
//3 执行完毕
//2 执行完毕
//5 执行完毕
//0 执行完毕
//全部完成
协程数量:
- untime包:运行时系统交互操作,例如控制 goroutines 的函数。其中比较有用的几个方法
- Gosched:让当前goroutine让出 cpu 以让其它goroutine运行,它不会挂起当前goroutine
- NumCPU:返回当前系统的 CPU 核数
- GOMAXPROCS:设置最大的可同时使用的 CPU 核数
- Goexit:退出当前 goroutine(但是defer语句会照常执行)
- NumGoroutine:返回正在执行和排队的任务总数
文件 /Users/go/src/com.test/appmain/main.go
package main
import (
"com.test/models"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"net/http"
"time"
"sync"
)
func main() {
var wg sync.WaitGroup
// 读取网络 url
url:="https://news.cnblogs.com/n/page/%d/" // %d 占位符
// 协程执行的结果不是按顺序完成,用map
// c := make(chan map[int][]byte)
for i:=1; i<=20; i++ {
go func(index int) {
defer func() {
if err:=recover();err!=nil{
fmt.Println(err) // 异常打印
}
wg.Done()
}()
url :=fmt.Sprintf(url, index) //格式化字符串,生成每一页的页码url
res, _ := http.Get(url) //利用http库 读取网页内容,返回response对象
cnt, _ := ioutil.ReadAll(res.Body) //读出response的body部分
// c <- map[int][]byte{index:cnt} //把结果构建成一个map 插入到 channel
ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", index), cnt, 777)
}(i)
wg.Add(1)
}
wg.Wait()
fmt.Println("抓取完毕")
//result := map[int][]byte{}
//myloop:for {
// select {
// case result =<-c: // 赋值变量
// for k, v := range result {
// // 把内容写入到文件中
// ioutil.WriteFile(fmt.Sprintf("src/com.hua/files/%d", k), v, 777)
// }
//
// case <-time.After(time.Second*3): // 只读
// break myloop // 跳出标签
//
//
// }
//}
}
9.8 互斥锁入门、io、bufio读写文件;
9.8.1 复习
// 单进程
list := make([]int, 0)
for i :=0; i<=10; i++ {
list = append(list, i)
}
fmt.Println(len(list)) // [0 1 2 3 4 5 6 7 8 9 10]
// 两个协程开启(每次结果不一样,不是按顺序执行)
// 使用得变量是全局共有变量,没有使用 channel 去传递
// 循环执行到一定时间,协程调度会自动把 CPU 切换(让出),称之为协程的不安全性
// i 设置的比较大(1000000),协程执行到 800000 的时候让出
// 但是我们希望执行完之后,再有其它协程来操作,那就要让同一个协程执行,就要用到锁
var wg sync.WaitGroup
list:=make([]int,0)
go func() {
defer wg.Done()
for i:=1;i<=10;i++{
list=append(list,1)
}
fmt.Println(len(list))
}()
go func() {
defer wg.Done()
for i:=1;i<=10;i++{
list=append(list,1)
}
fmt.Println(len(list))
}()
wg.Add(2)
wg.Wait()
fmt.Println(len(list)) // 10 10 10 随机且不准确
sync包,利用其 waitgroup 来实现等待协程执行结束
- sync包里还有一个很有用的功能:锁
- sync.Mutex,互斥锁,其中有两个函数:Lock() 上锁、Unlock() 解锁
- 一旦上锁后,其他协程来操作则必须等待 Unlock(过程是阻塞的),这样就能防止竞争资源导致的数据不准确
// 使用互斥锁,性能会适当下降
var wg sync.WaitGroup
var mutex sync.Mutex // 互斥锁
list:=make([]int,0)
go func() {
defer wg.Done()
defer mutex.Unlock()
mutex.Lock() // 上锁,执行的时候其它的协程无法执行
for i:=1;i<=1000000;i++{
list=append(list,1)
}
fmt.Println(len(list))
}()
go func() {
defer wg.Done()
defer mutex.Unlock()
mutex.Lock()
for i:=1;i<=1000000;i++{
list=append(list,1)
}
fmt.Println(len(list))
}()
wg.Add(2)
wg.Wait()
fmt.Println(len(list)) // 1000000 2000000 2000000
9.8.2 读写分离:怎么按行读取?需要快速了解几个包
-
OS包,标准包之一,用它可以来获取文件、目录、进程等
当前目录:os.Getwd()
获取当前进程ID:os.Getpid()
获取环境变量:os.Getenv(“PATH”))
创建文件:os.Create
创建目录:os.Mkdir
打开文件,获取文件指针:os.Open -
读取文件:
file,_:=os.Open("./test/news")
其中go里面有两个接口:io.Reader、io.Writer
它们是IO操作的两个高度抽象接口 -
实现Reader/Writer的常用结构有
1、net.Conn, os.Stdin, os.File: 网络、标准输入输出、文件
2、strings/bytes.Reader: 把字符串 byte[] 抽象成 Reader
3、bytes.Buffer: 把 []byte 抽象成 Reader 和 Writer
4、bufio.Reader/Writer: 抽象成带缓冲的流读取(比如按行读写)
// 在控制台输出我的名字,可以用以下“复杂”的代码来实现同样的效果
var myname bytes.Buffer
myname.Write([]byte("name"))
myname.WriteTo(os.Stdout)
// 同样这种方式还可以用来写文件
file,_:=os.OpenFile("./test/test",os.O_RDWR|os.O_CREATE | os.O_APPEND,666)
file.Write([]byte("name"))
defer file.Close()
// 或读文件
var buf []byte=make([]byte,512)
file,_:=os.OpenFile("./test/test",os.O_RDONLY,666)
file.Read(buf) // 仅仅是简单读
defer file.Close()
O_RDONLY int = syscall.O_RDONLY // 读模式
O_WRONLY int = syscall.O_WRONLY // 写模式
O_RDWR int = syscall.O_RDWR // 读写
O_APPEND int = syscall.O_APPEND // 在文件末尾追加,打开后cursor在文件结尾位置
O_CREATE int = syscall.O_CREAT // 如果不存在则创建
O_EXCL int = syscall.O_EXCL //与O_CREATE一起用,构成一个新建文件的功能,它要求文件必须不存在
O_SYNC int = syscall.O_SYNC // 同步方式打开,内容直接写入硬盘
O_TRUNC int = syscall.O_TRUNC // 打开并清空文件
bufio缓冲区
file,_:=os.OpenFile("./test/test",os.O_RDWR|os.O_CREATE | os.O_APPEND | os.O_TRUNC,666)
defer file.Close()
for i:=0;i<=10;i++{
file.Write([]byte("shenyi"))
time.Sleep(time.Second*3)
}
// 每隔3秒写入,我们可以不断打开文件测试,可以看到,每次 write 都是直接写入文件的
// 因此,频繁的对磁盘读写会造成不必要的性能开支和对磁盘的影响。
// 于是bufio出现了,它自带缓冲区,当缓冲到达一定量后才进行下一步操作
文件 /Users/go/src/com.test/appmain/main.go
package main
import (
_ "github.com/go-sql-driver/mysql"
"os"
"bufio"
"time"
)
func main() {
//file,_:=os.OpenFile("src/com.hua/files/test",os.O_RDWR|os.O_CREATE | os.O_APPEND | os.O_TRUNC,777)
//defer file.Close()
//for i:=1;i<=10;i++{
// file.Write([]byte("hua\n")) // 简单写入
// time.Sleep(time.Second*2)
//}
file,_:=os.OpenFile("src/com.hua/files/test",os.O_RDWR|os.O_CREATE | os.O_APPEND | os.O_TRUNC,666)
defer file.Close()
// 如果不做设置,缓冲区就是 4096 字节
// fw := bufio.NewWriterSize(file, 3) // fw.Flush() 可以不写
fw := bufio.NewWriter(file)
for i:=1;i<=10;i++{
fw.Write([]byte("huahua\n")) // 简单写入
//time.Sleep(time.Second*2)
}
fw.Flush()
}
9.9 多协程利用互斥锁按顺序、按行读文件;
接上一章,读取,文件 /Users/go/src/com.test/appmain/main.go
package main
import (
"io"
"sync"
_ "github.com/go-sql-driver/mysql"
"os"
"bufio"
"time"
)
func main() {
var wg sync.WaitGroup
var locker sync.Mutex
file,_:=os.OpenFile("src/com.hua/files/test",os.O_RDONLY,666)
defer file.Close()
fw := bufio.NewReader(file)
for i := 1;i <= 2; i++ {
go func(index int) {
defer wg.Done()
for {
locker.Lock()
str, err := fw.ReadString('\n') // 读取换行,byte使用单引号
if err != nil {
// 读到结尾
if err ==io.EOF {
locker.Unlock()
break
}
fmt.Println(err)
}
time.Sleep(time.Microsecond*200)
fmt.Printf("协程%d:%s", index, str)
locker.Unlock()
}
}(i)
}
wg.Add(2)
wg.Wait()
fmt.Println("读取完成")
// 协程2:1huahua
// 协程2:2huahua
// 协程2:3huahua
// 协程2:4huahua
// 协程2:5huahua
// 读取完成
}