范围(Range)
range 关键字用于 for 循环中迭代字符串、数组(array)、切片(slice)、通道(channel)和集合(map)等的元素。
迭代数组和切片时它返回元素的索引和索引对应的值
迭代集合时返回 key-value对,集合是无序的,返回的键值对顺序也是不确定的
迭代字符串时返回每个字符的索引(从0开始)和 Unicode值(rune)
迭代通道时返回从通道接收的值,直到通道关闭
在遍历时,可以使用 _ 来忽略索引或值
Go
//for 循环的 range 格式可以对 切片、集合、数组、字符串等进行迭代循环
for key, value := range oldMap {
newMap[key] = value
}
反射
反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力。
在反射的概念中,编译时就知道变量类型的是静态类型(static type);运行时才知道变量类型的是动态类型(concrete type)。
使用
反射是一种机制,用来的检测存储在接口变量内部pair对(值value,类型type)的一种机制,配合空接口使用
一般使用reflect包,比较常用的是ValueOf和TypeOf方法。
Go
func ValueOf (i interface{}) Value {...}
//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
func TypeOf(i interface{}) Type {...}
//翻译一下,TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil
reflect.TypeOf:直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似 &{1 "Allen.Wu" 25} 这样的结构体struct的值
也就是说明反射可以将"接口类型变量"转换为"反射类型对象",反射类型指的是reflect.Type和reflect.Value这两种。
结构体标签
json的基本序列化
序列化直接调用json.Marshal()和反序列化调用json.Unmarshal即可
序列化:结构体->json
反序列化:json->结构体
Go
package main
import (
"encoding/json"
"fmt"
)
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"星爷", "张柏芝"}}
//编码过程 结构体->json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error", err)
return
}
fmt.Printf("jsonStr = %s\n", jsonStr)
//解码过程 jsonstr->结构体
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Printf("%v\n", myMovie)
}
json与结构体标签tag
Tag标签是结构体的元信息,一般是在运行的时候通过反射的机制来读取出来。 Tag通常是在结构体字段的后方定义,用反引号包阔起来,其格式如下:
bash
`key1:"value1" key2:"value2"`
结构体标签可以有多个键值对组成。键与值使用冒号分隔,值用双引号括起来。
json的序列化与反序列化默认情况下会使用结构体的字段名,可以通过给结构体字段添加tag来指定json序列化生成的字段名。
Go
// 使用json tag指定序列化与反序列化时的行为
type Person struct {
Name string `json:"name"` // 指定json序列化/反序列化时使用小写name
Age int64
Weight float64 `json:"-"`// 指定json序列化/反序列化时忽略此字段
}
错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error 类型是一个接口类型,这是它的定义:
Go
type error interface {
Error() string
}
并发(多线程)
进程
进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为"正在执行的程序",它是CPU资源分配和调度的独立单位。
进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。
线程(内核空间)
线程是在进程之后发展出来的概念。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。
线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生"死锁"。
进程/线程的数量越多,切换成本就越大,也就越浪费
协程(用户空间)
协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。
子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。
与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。
一个线程可以绑定多个协程
Goroutine
使用Goroutine来实现并发concurrently,Goroutine是Go语言特有的名词。区别于进程Process,线程Thread,协程Coroutine,因为Go语言的创造者们觉得和他们是有所区别的,所以专门创造了Goroutine。
Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。
Goroutines在线程上的优势
- 与线程相比,Goroutines非常便宜。它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且是固定的
- Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。如果线程中的任何Goroutine都表示等待用户输入,则会创建另一个OS线程,剩下的Goroutines被转移到新的OS线程。所有这些都由运行时进行处理,我们作为程序员从这些复杂的细节中抽象出来,并得到了一个与并发工作相关的干净的API。
- 当使用Goroutines访问共享内存时,通过设计的通道可以防止竞态条件发生。通道可以被认为是Goroutines通信的管道
主Goroutine
封装main函数的goroutine称为主goroutine。
主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随后,这个go程序的运行也会终止。
此后,主goroutine会进行一系列的初始化工作,涉及的工作内容大致如下:
- 创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束
- 启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识
- 执行mian包中的init函数
- 执行main函数
- 执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。最后主goroutine会结束自己以及当前进程的运行。
使用Goroutine
在函数或方法调用前面加上关键字go,将会同时运行一个新的Goroutine
Go
go 函数名(参数列表)
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。
同一个程序中的所有 goroutine 共享同一个地址空间。
Go
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
//输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行
通道(channel)
通道(channel)是用来传递数据的一个数据结构,是Goroutines通信的管道。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
使用 make 函数创建一个 channel,使用 操作符发送和接收数据。如果未指定方向,则为双向通道。
Go
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据并把值赋给v
v, ok := <-channel // 从 ch 接收数据并把值赋给v,同时检查通道是否已关闭并赋值给ok
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
Go
ch := make(chan int)//声明一个存储整数类型的通道
注意
- 每个通道都有相关联的数据类型,nil chan不能使用(类似于nil map不能直接存储键值对)
- 阻塞
- 没有缓冲区的情况下:
发送数据:chan<-data,发送完当前线程立即阻塞,直到另一条goroutine,读取数据来接触阻塞
读取数据:data<-chan,读取完当前线程立即阻塞,直到另一条goroutine,写入数据来解除阻塞
- 有缓冲区的情况下:
缓冲区没满则不会发生阻塞(向通道发送和从通道中获取这两个行为,既不是同步的也不会相互阻塞)
缓冲区空时读数据会阻塞,缓冲区满时写数据会发生阻塞
3.同步:本身channel就是同步的,意味着同一时间只能有一条goroutine来操作
4.通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中
通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
Go
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
通道关闭(关闭后不能发只能收)
- channel不像文件一样需要经常去关闭,只有当没有任何发送数据了,或者想显示的结束range循环之类的,才去关闭channel
- 关闭channel后,无法向channel再发送数据(否则引发panic错误后导致接收立即返回零值)
- 关闭channel后,可以继续从channel接收数据
- 对于nil channel,无论收发都会被阻塞
Select语句
单流程下,一个go只能监控一个channel的状态,select可以完成监控多个channel的状态。
- 检查所有case:select 会同时检查所有的 case 语句(执行所有case),看是否有可以立即执行的操作。
- 随机选择:如果有多个 case 同时准备好(即可以立即执行),select 会随机选择其中一个来执行。
- 阻塞等待:如果没有 case 准备好,select 会阻塞,直到某个 case 准备好为止。
- 执行默认case:如果有 default 语句,当没有任何 case 准备好时,会执行 default 语句
Go
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: //如果c可写入,则进行该case语句
//将x发送到c通道,并更新x和y
x, y = y, x+y
case <-quit: 如果quit可读,则进行该case语句
//从quit通道中获取数据,并打印quit,随后结束函数
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() { //使用匿名函数开启一个goroutine
for i := 0; i < 10; i++ {
fmt.Println(<-c) //
不断地从c通道中读10个数据并打印
}
quit <- 0 //将0发送到quit通道
}()
fibonacci(c, quit)
}
- 非阻塞:如果所有的case都阻塞,select 将阻塞。但如果至少有一个case可以立即执行,select 将选择这个case并执行相应的代码块。
- 随机性:如果有多个case可以立即执行,select 将随机选择其中一个来执行。
- default子句:如果没有case可以立即执行,并且存在default子句,select 将执行default中的代码。如果没有default子句,select 将阻塞。
- 发送和接收:select 可以同时处理channel的发送和接收操作。
- 死锁避免:select 可以帮助避免死锁,因为它确保至少有一个case可以执行,从而避免了所有goroutine都在等待其他goroutine发送或接收数据的情况。