18、反射
变量 :
内在机制在于 ,可以分为 类型信息 与 值信息 ,前者存储的是预先定义好的元信息 ,后者存储的是动态变化内容
反射 :
指的是在程序运行期间 对程序本身进行访问与修改的能力
程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们
本质 : 反射就是在运行时动态的获取一个变量的类型信息和值信息
reflect 包 :
任何接口值 ,都可以理解为 reflect.type 与 reflect.value 组成
typeof:
获取任意值的类型 reflect.type ,其中类型又可以分为 name 与kind两类,kind更加基础,指底层的类型
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
valueof:
返回的是 reflect.value 类型,并且包含了原始值的信息 ,可以相互转换
方法 | 说明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
通过反射来设置值:
由于函数传参是值传递的 ,因此如果直接使用setint()会引发panic,有专门的elem()来进行取地址
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
isnil() :判断指针是否为空
isvalid() : 判断返回值是否有效 reflect.ValueOf(b).FieldByName(“abc”).IsValid() 判断字段是否存在
结构体反射:
结构体的方法一定要大写
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 |
NumField() int | 返回结构体成员字段数量。 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据传入的匹配函数匹配需要的字段。 |
NumMethod() int | 返回该类型的方法集中方法的数目 |
Method(int) Method | 返回该类型方法集中的第i个方法 |
MethodByName(string)(Method, bool) | 根据方法名返回该类型方法集中的方法 |
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
v.Method(i).Call(args)
19、并发
并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。
并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。
goroutine类似于线程,属于用户态的线程,可以根据需要创建很多goroutine
goroutine由go的runtime(运行时)调度完成 ,线程则由操作系统调度完成
多个goroutine之间 通过channel 进行通信
CSP(Communicating Sequential Process)并发模式 :通信顺序进程
main函数当中 就是一个主goroutine ,当主goroutine结束之后 ,当中的别的goroutine直接结束
启动多个goroutine时 ,可以使用time.sleep ,也可以使用sync.waitgroup
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB
GMP:
区别于操作系统调度os线程,GMP是运行时runtime调度的
G
很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。M(machine)
是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;P
管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M
P的个数是通过runtime.GOMAXPROCS
设定(最大256 )
共享内容实现通信容易发生数据竞态问题,只能通过互斥锁来保护数据,性能低下
Go语言的并发模型是CSP(Communicating Sequential Processes)
,提倡通过通信共享内存而不是通过共享内存而实现通信。
channel:
var 变量 chan 元素类型 , 引用类型
var ch chan int
ch = make(chan int)
...
ch1 := make(chan bool,10)
cn2 := make(chan []int )
三个操作 : 发送 , 接受 ,关闭
ch <- 10
x := <- ch 或者 <-ch
close(ch)
无缓冲的channel: 无缓冲的channel在执行发送操作时 会阻塞
ch := make(chan int) 只能在有channel接受值的时候才能发送值
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
有缓冲的channel : make 指定channel大小
ch := make(chan int,10)
len(ch) cap(ch)
一些注意:
channel关闭之后再发送会引发panic ,关闭之后再关闭也会引发panic
channel关闭之后取值会将值取完,再取就会取对应类型的零值
如何取值:
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
.....
for {
i, ok := <- ch
if !ok{
break
}
fmt.Println(i)
}
还有单向通道:
func f1(out <-chan int, in chan<- int ) {}
<-chan 表示只能从通道当中取值 ,只读
chan<- 表示只能往通道当中写入值 ,只写
goroutine池 (worker pool):
worker pool 当中可以指定开启的goroutine数量,防止goroutine泄露或者是暴涨 ,小例子:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker:%d start job:%d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker:%d end job:%d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启3个goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// 输出结果
for a := 1; a <= 5; a++ {
<-results
}
}
select 多路复用:
场景 : 同时从多个channel当中接收数据 ,多个满足的case当中随机选择一个执行,否则阻塞等待
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
并发安全与锁:
当多个goroutine同时操作一个临界区时,会发送数据竞态问题
使用互斥锁可以保证同一个时间 只有一个goroutine进入临界区,其他的goroutine等待当前互斥锁的释放
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
读写互斥锁:
读多写少的场景下,应该使用sync.RWMutex,可以分为读锁与写锁,当一个goroutine获得读锁之后,其余goroutine如果获取读锁直接获取,获取写锁则等待,当一个goroutine获取写锁之后,其余goroutine无论获取读锁还是写锁都要等待
sync.waitgroup
sync.WaitGroup
内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成
sync.once 只执行一次的操作 func (o *Once) Do(f func()) {}
var loadIconsOnce sync.Once
....
loadIconsOnce.Do(func1())
基于sync.once的单例 :
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
sync.map 线程安全的map
store ,load 等函数
20、网络编程
socket : IP + PORT
TCP:
server :
net.listen 建立socket
listen.accept 建立连接
启动一个goroutine 进行处理
// tcp/server/main.go
// TCP server端
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
client:
net.dial 建立socket
conn.read conn.write
// tcp/client/main.go
// 客户端
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
粘包问题 :
“粘包”可发生在发送端也可发生在接收端:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
UDP :
server:
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
client:
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
21、单元测试
TDD(Test Driven Development)
_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
测试函数 :
func TestName(t *testing.T){
// ...
}
可以使用 reflect.deepequal 进行对比
直接执行 go test 就行
go test -cover -coverprofile=c.out 测试覆盖率
基准测试:
func BenchmarkSplit(b *testing.B) {
//b.N不是固定的数字
for i:=0;i<b.N ;i++
{ Split("三天三夜","三")
}
}
示例函数:
func ExampleSplit() {
fmt.Println(split.Split("a:b:c", ":"))
fmt.Println(split.Split("沙河有沙又有河", "沙"))
// Output:
// [a b c]
// [ 河有 又有河]
}
包含 //Output