1. 并发编程
1.1 并发和并行
并发: 多个线程在同个核心的CPU上运行.并发的本质是串行.
并行: 多个线程在多个核心的CPU上运行.
1.2 协程和线程
协程: 独立的栈空间,共享堆空间,调度由用户控制,本质上有点类似用户及线程,这些用户及线程的调度也是自己实现的.
线程: 一个线程上可以跑多个协程,协程是轻量级的线程.(操作系统调度的)
1.3 goroutine
Go 语言中goroutine就是一种机制,类似于线程,但它是由Go运行时(runtime)调度和管理
Go程序会智能地将goroutine中的任务合理分配给每个CPU
Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换机制.
在Go语言编程中你不需要自己去写进程,线程,协程,你的技能包里只要有一个goroutine就可以.
当你需要让某个任务并发执行时,只需要把这个任务包装成一个函数.
开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴.
1.4 协程的使用
正常情况下
func main() {
test()
}
func test() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
结果
0
1
2
3
4
5
6
7
8
9
开启协程方法1:
go 方法名()
go test()
可以看到这里main和test是一起打印的.
func main() {
go test()
for i := 0; i < 10; i++ {
fmt.Println("main", i)
time.Sleep(time.Microsecond * 100)
}
time.Sleep(time.Second)
fmt.Println("done")
}
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test", i)
time.Sleep(time.Microsecond * 100)
}
}
结构:
main 0
test 0
test 1
main 1
main 2
test 2
main 3
test 3
main 4
test 4
main 5
test 5
test 6
main 6
main 7
test 7
test 8
main 8
main 9
test 9
done
1.4.2 sync.WaitGroup
线程开启时候协程
goroutine 开启wait.add(1) 计数器加1
goroutine结束wait.Done()计数器减1
groutine退出wait.wait()判断当前grouproutine是否为0,为0就退出.
// 1. 定义计数器
var wait sync.WaitGroup
func main() {
// 2.开启一个协程计算器+1
wait.Add(1)
go test()
// 4.计算器为0时退出
wait.Wait()
fmt.Println("Done!")
}
func test() {
for i := 0; i < 10; i++ {
fmt.Println("main", i)
time.Sleep(time.Microsecond * 100)
}
// 3.协程执行完毕,计数器-1
wait.Done()
}
2. channel
2.1 Channel说明
-
共享内存交互数据弊端
- 单纯地将函数并发执行是没有意义的,函数与函数间需要交换数据才能体现并执行函数的意义.
- 虽然可以使用共享内存进行数据交互,但是共享内存在不同的goroutine中容易发生竞态问题.
- 为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题.
-
channel好处
- Go语言中的通道(channel)是一种特殊的类型.
- 通道像一个传送带或者队列,总是遵循先进先出规则,保证收发数据的顺序.
- 每一个通道都是一个具体类型的管道,也就是声明channel的时候需要为其指定元素类型.
- goroutine并发执行时,channel就是他们之间的连接
- channel是让一个goroutine发送特定的值到另一个goroutine的通讯机制
2.2 channel类型
var 变量 chan 元素类型
var ch1 chan int //整形管道
var ch2 chan bool //布尔型
var cha3 chan []int //切片管道
func main() {
// 1. 定义channel
// make 可以给切片,map,channel分配内存
// chan 关键字,int channel类型,5 channel的长度大小,就是最多可以往ch1里存多少个数据,如果存第6个就会出错.
ch1 := make(chan int, 5)
// 2. 向channel存入数据
ch1 <- 10
// 3. 从channel取数据
v1 := <-ch1
fmt.Println("v1", v1)
// 4. 空channel且没有关闭 取值会报错
}
结果:
v1 10
2.3 channel 循环取值
func main() {
ch1 := make(chan int, 5)
ch1 <- 1
ch1 <- 2
ch1 <- 3
ch1 <- 4
ch1 <- 5
close(ch1)
for i := range ch1 {
fmt.Println(i)
}
}
结果:
1
2
3
4
5
如果没有close就会报错
fatal error: all goroutines are asleep - deadlock!
2.4 select 多路复用
-
select说明
- 传统的方法遍历管道时,如果不关闭会阻塞而导致deadlock,在实际开发中,我们不能确定具体什么时间该关闭管道.
- 这种方式可以实现从多个管道接收值的需求,但运行性能会差很多
- 为了应对这种场景,Go内置了select关键字,可以同时响应多个管道的操作.
- select使用类似switch语句,他有一系列case分支和一个默认的分支.
- 每个case会对应一个管道的通信(接收和发送)过程.
- select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句.
func main() { // 1. 定义channel ch1 := make(chan int, 10) for i := 0; i < 10; i++ { ch1 <- i } ch2 := make(chan string, 10) for i := 0; i < 10; i++ { ch2 <- strconv.Itoa(i) } for { select { case v := <-ch1: fmt.Println("int channel", v) time.Sleep(100 * time.Millisecond) case s := <-ch2: fmt.Println("string channel", s) time.Sleep(100 * time.Millisecond) default: fmt.Println("Channel 中数据已经取完.") return } } } 结果 int channel 0 string channel 0 int channel 1 string channel 1 int channel 2 string channel 2 int channel 3 string channel 3 int channel 4 string channel 4 int channel 5 int channel 6 int channel 7 string channel 5 string channel 6 int channel 8 int channel 9 string channel 7 string channel 8 string channel 9 Channel 中数据已经取完.
3. 互斥锁
- 互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源.
- Go语言中使用sync包的Mutex类型来实现互斥锁
var x int func main() { fmt.Println(x) add() fmt.Println(x) } func add() { for i := 1; i <= 5000; i++ { x += 1 } } 结果 0 5000
当开启了多个协程对一个资源进行操作,就出现了资源竞争.
var x int var wg sync.WaitGroup func main() { wg.Add(2) fmt.Println(x) go add() go add() wg.Wait() fmt.Println(x) } func add() { for i := 1; i <= 5000; i++ { x += 1 } wg.Done() } 结果 0 7606 第二次运行结果是 0 8813
为了保证数据正常,需要加上互斥锁.
var x int var wg sync.WaitGroup // 1. 定义互斥锁 var lock sync.Mutex func main() { wg.Add(2) fmt.Println(x) go add() go add() wg.Wait() fmt.Println(x) } func add() { for i := 1; i <= 5000; i++ { // 2. 执行前加锁 lock.Lock() x += 1 // 3. 执行完解锁 lock.Unlock() } wg.Done() } 结果 0 10000
3. fmt
常用占位符
参数 | 功能 |
---|---|
%v | 按值的本来值输出 |
%+v | 在%v基础上,对结构体字段名和值进行展开 |
%#v | 输出go语言语法格式的值 |
%T | 类型的值 |
%% | 输出%%本体 |
%b | 以二进制显示 |
%o | 以8进制显示 |
%d | 以10进制显示 |
%x | 以16进制显示 |
%X | 以16进制显示,字母大写 |
%U | Unicode字符 |
%f | 浮点数 |
%p | 指针,16进制方式显示 |
3.1 Sprint
将格式化的数据复制给其他变量
s := fmt.Sprintf("姓名: %s age: %d","张三",24)
fmt.Println(s)
结果
姓名: 张三 age: 24
fmt.Printf 不换行
fmt.Println 换行
4. 时间
4.1 时间转换
- 时间对象, golang中定义的一个对象
- time.Now()
- 时间戳: 秒整数形式,1970年1月1日开始
- now.Unix()
- 格式化时间:人看
- now.Format(“2006-01-02 15:04:05”)
func main() {
// 1. 获取时间对象
now := time.Now()
fmt.Printf("%T %v\n", now, now)
// 2. 格式化时间 将时间对象,转换为格式化的时间
strTime := now.Format("2006-01-02 15:04:05")
fmt.Printf("%T %v\n", strTime, strTime)
// 3. 时间戳格式 秒的整数形式
ts := now.Unix()
fmt.Printf("%T %v\n", ts, ts)
// 4. 格式化时间转换成时间对象
// 4.1 设置时区
loc, _ := time.LoadLocation("Asia/Shanghai")
// 4.2 传入时间标记2006-01-02 15:04:05 这个值是不能修改的
timeObj, _ := time.ParseInLocation("2006-01-02 15:04:05", strTime, loc)
fmt.Println(timeObj.Unix())
}
结果
time.Time 2022-12-05 14:00:40.2687082 +0800 CST m=+0.004188601
string 2022-12-05 14:00:40
int64 1670220040
1670220040
4.2 时间类型
func main() {
now := time.Now()
year := now.Year()
month := now.Month()
day := now.Day()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
fmt.Printf("%v %v %v %v %v %v", year, month, day, hour, minute, second)
}
// %02d 保留2位十进制数字,不够就高位补0
Today := fmt.Sprintf("%02d-%d-%02 %d:%d:%d", year, month, day, hour, minute, second)
fmt.Printf("%T %v", Today, Today)
结果
2022 December 5 14 10 53
2022-12-05 14:15:11
string
4.3 时间间隔
参数 | 含义 |
---|---|
nanosecond | 纳秒 , 十亿分之一秒 |
Microsecond | 1000*nanosecond微秒,一百万分之一秒 |
Millisecond | 1000*Microsecond毫秒,千分之一秒 |
Second | 1000*Microsecond,秒 |
Minute | 60*second,分 |
Hour | 60*Minute,小时 |
4.3.1 Add方法
func main() {
now := time.Now()
fmt.Println("现在是:", now)
m, _ := time.ParseDuration("-1m")
m1 := now.Add(m)
fmt.Println("前1分钟是:", m1)
}
结果
现在是: 2022-12-05 14:42:46.8769096 +0800 CST m=+0.003688201
前1分钟是: 2022-12-05 14:41:46.8769096 +0800 CST m=-59.996311799
5. Flag
Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单.
func main() {
// 1. String variables
var name string
var address string
flag.StringVar(&name, "name", "张三", "姓名")
flag.StringVar(&address, "address", "上海", "地址")
flag.Parse()
fmt.Println(flag.Args())
}
在命令行下执行--help
PS D:\golang\day3\03.flag> go run main.go --help
Usage of C:\Users\Q\AppData\Local\Temp\go-build1568490171\b001\exe\main.exe:
-address string
地址 (default "上海")
-name string
姓名 (default "张三")
命令行传参
&name 变量的指针,传入的数据赋值给他
name 命令行里的key
“张三” 如果不传递张三就作为默认值
“姓名” --help里的提示信息.
func main() {
// 1. String variables
var name string
var address string
flag.StringVar(&name, "name", "张三", "姓名")
flag.StringVar(&address, "address", "上海", "地址")
flag.Parse()
fmt.Println(name, address)
}
命令行执行,如果不传值,就会用默认值替代
PS D:\golang\day3\03.flag> go run main.go -name "李四" -address "北京"
李四 北京
PS D:\golang\day3\03.flag> go run main.go -name "李四"
李四 上海
fmt.Println(name, address)
// Args 可以接收除了name和address以外的传入变量
fmt.Println(flag.Args())
结果
PS D:\golang\day3\03.flag> go run .\main.go 1 3 2 4
张三 上海
[1 3 2 4]
6. net-http
它既能提供server端,又能提供client端.
6.1 Get请求
方法名称 | 描述 |
---|---|
Header() | 用户设置或获取响应头信息 |
write() | 用于写入数据响应体 |
WriteHeader() | 用于设置响应状态码,若不调用则默认状态码为200 OK. |
6.1.1 返回数据
启动一个http服务,进行简单的返回一个数据
/*
1. 路由
2. 处理函数
1. 解析请求数据
2. 处理函数将结果进行返回
3. 启动服务
*/
func main() {
// 1. 定义路由
http.HandleFunc("/req/get", dealGetHandler)
fmt.Println("http://127.0.0.1:8080/req/get")
// 3. 启动服务
// addr: 当前server监听的端口号,handler:处理函数
http.ListenAndServe(":8080", nil)
}
// 2. 定义处理函数,用驼峰命名,以xxxHandler为函数名
// Get 请求
// http.ResponseWriter 返回数据给浏览器的,本质是一个interface接口,定义了三个方法,进行返回数据
// *http.Request 将传过来的参数放入Request结构体中. 解析url中的数据或post请求body的数据
func dealGetHandler(w http.ResponseWriter, r *http.Request) {
// 直接返回数据
w.Write([]byte("hello world"))
}
6.1.2 解析数据
// 1. 解析请求的数据
query := r.URL.Query()
fmt.Println(query)
当浏览器输入http://127.0.0.1:8080/req/get?name=zhangsan时,得到以下返回
http://127.0.0.1:8080/req/get?name=zhangsan
6.1.3 通过get取值
func deal2GetHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name2 := query.Get("name")
fmt.Println(name2)
// 直接返回数据
w.Write([]byte("hello world"))
}
6.1.4 返回json值
func deal2GetHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name2 := query.Get("name")
fmt.Println(name2)
// 1.1 直接返回字符串
//w.Write([]byte("hello world"))
// 1.2 返回json
type Info struct {
Name string
Password string
Age int64
}
u := Info{
Name: name2,
Password: "123456",
Age: 18,
}
json.NewEncoder(w).Encode(u)
}
返回内容:
{"Name":"张三","Password":"123456","Age":18}
6.2 Post请求
func main() {
// 1. 定义路由
http.HandleFunc("/req/get", deal2GetHandler)
http.HandleFunc("/req/post", dealPostHandler)
fmt.Println("http://127.0.0.1:8080/req/get")
// 3. 启动服务
// addr: 当前server监听的端口号,handler:处理函数
http.ListenAndServe(":8080", nil)
}
// Get部分省略
// 和Get请求一样写法
func dealPostHandler(w http.ResponseWriter, r *http.Request) {
// r.URL.query() 从url取参数
// post 从http的body取中获取数据
bodyContent, _ := ioutil.ReadAll(r.Body)
fmt.Printf("%T %v\n", bodyContent, bodyContent)
w.Write([]byte("hello Post"))
}
6.2.1 解析POST传入的值
- 通过iouttil.ReadAll 读出http.Request.body的结构体
- 通过json的绑定,将数据绑定到定义的结构体
func dealPostHandler(w http.ResponseWriter, r *http.Request) {
// r.URL.query() 从url取参数
// post 从http的body取中获取数据
bodyContent, _ := ioutil.ReadAll(r.Body)
// uint8 转string
//strData := string(bodyContent)
// string 转结构体
// 定义个一个格式一样的struct
var d Info2
json.Unmarshal(bodyContent, &d)
// 获取到的name的数据
fmt.Println(d.Name, d.Password)
//fmt.Printf("%T %v\n", bodyContent, strData)
w.Write([]byte("hello Post"))
}
type Info2 struct {
Name string `json:"name"`
Password string `json:"password"`
}
结果
zhangsan root123
6.3 请求数据
6.3.1 Get方法
- 通过body进行解析
func main() {
apiUrl := fmt.Sprintf("http://127.0.0.1:8080/req/get?name=zhangsan")
resp, err := http.Get(apiUrl)
if err != nil {
fmt.Println(err)
return
}
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
var b Info
json.Unmarshal(body, &b)
fmt.Println(b.Name)
}
type Info struct {
Name string `json:"name"`
}
结果
{"Name":"zhangsan","Password":"123456","Age":18}
zhangsan
- 通过url进行解析
func main() {
//从这里开始
apiUrl := "http://127.0.0.1:8080/req/get"
data := url.Values{}
data.Set("name", "zhangsan1")
u, _ := url.ParseRequestURI(apiUrl)
u.RawQuery = data.Encode()
// 到这里其实就是在拼接url
// 后面的和之前的一样
resp, err := http.Get(u.String())
if err != nil {
panic(err)
}
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
var i Info
json.Unmarshal(body, &i)
fmt.Println(i.Name, i.Password, i.Age)
}
type Info struct {
Name string `json:"name"`
Password string `json:"Password"`
Age int `json:"Age"`
}
结果:
{"Name":"zhangsan1","Password":"123456","Age":18}
zhangsan1 123456 18
6.3.2 Post方法
func main() {
url := "http://127.0.0.1:8080/req/post"
// 表单数据提交 form submission
//contentType := "application/x-www-form-urlencoded"
// Json数据提交
contentType := "application/json"
data := `{
"name": "zhangsan",
"password": "root123"
}`
resp, _ := http.Post(url, contentType, strings.NewReader(data))
b, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(b))
}
结果:
hello Post
服务器端结果:
zhangsan root123
7. Os模块
func main() {
// 1. 获取当前目录
fmt.Println(os.Getwd())
// 2. 切换路径
os.Chdir("d:\\game\\")
fmt.Println(os.Getwd())
// 3. 创建文件夹
os.Mkdir("test", 0777)
// 4. 删除
//os.Remove("test")
// 5. 重命名
os.Rename("test", "test2")
// 6. 新建文件
os.Create("test2/test.txt")
}