Go 基础语法2(Mysql 调用、协程)

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
// 读取完成

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值