8 channel、反射、网络编程【Go语言教程】
1 channel
1.1 概念及快速入门
channel:管道,主要用于不同goroutine之间的通讯
需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。要求使用 goroutine 完成
分析思路:
- 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
- 这里就提出了不同 goroutine 如何通信的问题
package main
import(
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10)
)
//计算n!,将计算结果放入myMap
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res //concurrent map writes?
}
func main(){
//开启200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s[防止主线程直接跑完,而协程中的任务未完成]
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
运行代码会出现并发问题:
不同goroutine之间如何通讯:
- 全局变量的互斥锁
- 使用管道channel来解决
计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。用下面方法改进:
①使用全局变量的互斥锁[并发、并行问题]
没有对全局变量m加索,会出现资源争夺问题,提示:concurrent map writes
加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += i
package main
import(
"fmt"
"time"
"sync"
)
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包, synchronized 同步
//Mutex:是互斥
lock sync.Mutex
)
//计算n!,将计算结果放入myMap
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res += int(i)
}
//加锁
lock.Lock()
myMap[n] = res //concurrent map writes?
//解锁
lock.Unlock()
}
func main(){
//开启200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s[防止主线程直接跑完,而协程中的任务未完成]
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
②使用channel
1. 为什么需要channel
- 为什么需要channel?
- 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
- 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
- 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
- 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
- 上面种种分析都在呼唤一个新的通讯机制-channel
2. 基本介绍
- channle 本质就是一个数据结构-队列【示意图】
- 数据是先进先出【FIFO : first in first out】
- 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
- channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
3. 声明、定义channel
- var 变量名 chan 数据类型
举例:
var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型) var perChan chan Person
var perChan2 chan *Person
…- 说明:
channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用管道是有类型的,intChan 只能写入 整数 int
4. 快速入门及注意事项
(1)快速入门:
管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
package main
import (
"fmt"
)
func main(){
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan的值=%v intChan本身地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- -50
//intChan <- 100 //注意:我们在给管道写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//5. 从管道中读取数据
var num2 int
num2 = <- intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//6. 在没有使用协程的情况下,如果管道中的数据已经全部取出,再取就会报告deadlock
num3 := <- intChan
num4 := <- intChan
fmt.Printf("num3=%v, num4=%v", num3, num4)
// num5 := <- intChan
// fmt.Println("num5=", num5) //fatal error: all goroutines are asleep - deadlock!
}
(2)注意事项:
- channel 中只能存放指定的数据类型
- channle 的数据放满后,就不能再放入了
- 如果从 channel 取出数据后,可以继续放入
- 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
5. channel的使用
练习题:
package main
import (
"fmt"
"math/rand"
"time"
"strconv"
)
type Person struct {
Name string
Age int
Address string
}
func main(){
var personChan chan Person
//make给chan开辟空间
personChan = make(chan Person, 10)
//取纳秒时间戳作为种子,保证每次的随机种子都不同
//给rand种种子
rand.Seed(time.Now().UnixNano())
for i := 1; i <= 10; i++ {
index := rand.Int()
fmt.Println("index===", index)
person := Person{
Name: "zhangsan" + strconv.Itoa(index),
Age: i,
Address: "beijing" + strconv.Itoa(index),
}
personChan <- person
}
len := len(personChan)
for i := 0; i < len; i++ {
p := <- personChan
fmt.Println(p)
}
}
6. channel的关闭和遍历
(1)channel的关闭
使用内置函数close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 5)
intChan <- 10
intChan <- 20
close(intChan) //close
//关闭之后不能再向chan写入数据,但是可以读取
// intChan <- 30 //panic: send on closed channel
n1 := <- intChan
fmt.Println("n1=", n1) //n1= 10
}
(2)channel的遍历
channel 支持 for–range 的方式进行遍历,请注意两个细节
- 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 5)
intChan <- 10
intChan <- 20
intChan <- 50
close(intChan) //close
//关闭之后不能再向chan写入数据,但是可以读取
// intChan <- 30 //panic: send on closed channel
n1 := <- intChan
fmt.Println("n1=", n1) //n1= 10
for v := range intChan {
fmt.Printf("value=%v\n", v)
}
}
7. 综合实例
- 需求:
要求统计 1-8000的数字中,哪些是素数?使用 goroutine
和 channel 的知识完成- 分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。
package main
import (
"fmt"
"time"
)
//向intChan放入1-8000个数
func putNum(intChan chan int){
for i := 1; i <= 8000; i++ {
intChan <- i
}
//关闭chan
close(intChan)
}
//从intChan取出数据,并判断是否是素数,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool){
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok { //取不到数据了,就退出
break
}
flag = true //假设是素数
for i := 2; i < num; i++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据,退出")
//这里我们还不能关闭primeChan
//向exitChan写入true
exitChan <- true
}
func main(){
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
//标识退出的管道
exitChan := make(chan bool, 4)
//开启一个协程,向intChan放入1-8000个数
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否是素数
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程进行处理
go func(){
for i:= 0; i < 4; i++{
<-exitChan
}
//当我们从exitChan取出了4个结果,就可以放心的关闭primeChan
close(primeChan)
}()
//遍历primeChan,把结果取出
for {
res, ok := <- primeChan
if !ok {
break
}
//将结果取出
fmt.Printf("素数=%d\n", res)
}
}
结果:
8. 只读、只写管道及注意事项
- 只读、只写管道:
- 注意事项
- channel 可以声明为只读,或者只写性质
- 只读只写案例
3)使用 select 可以解决从管道取数据的阻塞问题
select用法:
package main
import (
"fmt"
"time"
)
func main(){
//使用select可以解决管道取数据的阻塞问题
//1. 定义一个管道 10 int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2. 定义一个管道 5 string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭,则会因阻塞导致deadlock
//可是我们在实际开发中,我们可能不好确定什么时候关闭管道
//办法:我们可以使用select方式解决
//label:
for {
select {
//注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
case v := <- intChan:
fmt.Printf("从intChan读取的数据=%d\n", v)
time.Sleep(time.Second)
case v := <- stringChan:
fmt.Printf("从stringChan读取的数据=%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("不玩了,都取不到了【程序员可以在这里加入自己的逻辑】\n")
time.Sleep(time.Second)
return
//break label
}
}
}
2 反射
2.1 概念
- 反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
- 通过反射,可以修改变量的值,可以调用关联的方法。
- 使用反射,需要 import (“reflect”)
5)反射常见的应用场景
2.2 反射中重要的函数
- 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到。
2.3 快速入门
请编写一个案例,演示对(结构体类型、interface{}、reflect.Value)进行反射的基本操作
package main
import (
"reflect"
"fmt"
)
type Student struct {
Name string
Age int
}
func reflectTest01(b interface{}){
//通过反射获取到传入变量的type、kind值
//1. 先获取到reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)
//2. 获取到reflect.Value
rVal := reflect.ValueOf(b)
n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)
fmt.Printf("rVal=%v rType=%T\n", rVal, rType)
//下面我们将rVal转成interface{}
iV := rVal.Interface()
//将interface{}通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
}
//对结构体的反射
func reflectTest02(b interface{}){
//通过反射获取到传入的变量的type、kind,值
//1. 先获取到reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)
//2. 获取到reflect.Value
rVal := reflect.ValueOf(b)
//下面我们将rVal转成interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T\b", iV, iV)
//将interface{}通过断言转成需要的类型
//这里,我们使用类型断言【同学们可以使用switch的断言形式来更加灵活的判断】
stu, ok := iV.(Student)
if ok {
fmt.Printf("stu.Name=%v\n", stu.Name)
}
}
func main(){
//1. 基本数据类型 反射
var num int = 100
reflectTest01(num)
//2. 定义一个Student实例
stu := Student{
Name: "tom",
Age: 20,
}
reflectTest02(stu)
}
2.4 反射的细节和注意事项
- reflect.Value.Kind,获取变量的类别,返回的是一个常量
- Type 和 Kind 的区别【type更加具体】
Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的,也可能是不同的. 比如: var num int = 10 num 的 Type 是 int , Kind 也是 int
比如: var stu Student stu 的 Type 是 pkg1.Student , Kind 是 struct
- 通过反射的来修改变量, 注意当使用 SetXxx 方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到reflect.Value.Elem()方法
- reflect.Value.Elem() 应该如何理解?
2.5 综合案例
使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
package main
import (
"reflect"
"fmt"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex string
}
//方法,返回两数的和
func (s Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
//方法,接收四个值,给结构体赋值
func (s Monster) Set(name string, age int, score float32, sex string){
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}
//方法,显示结构体的值
func (s Monster) Print(){
fmt.Println("----start----")
fmt.Println(s)
fmt.Println("----end----")
}
func TestStruct(a interface{}){
//获取reflect.Type类型
typ := reflect.TypeOf(a)
//获取reflect.Value类型
val := reflect.ValueOf(a)
//获取到a对应的类别
kd := val.Kind()
//如果传入的不是struct,就退出
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
//是结构体,获取该结构体有几个字段
num := val.NumField()
fmt.Printf("struct has %d fields\n", num)
for i := 0; i < num; i++ {
fmt.Printf("Field %d值为=%v\n", i, val.Field(i))
//获取到struct标签,注意需要通过reflect.Type来获取tag标签的值
tagVal := typ.Field(i).Tag.Get("json") //因为前面定义结构体用到了'json标签'
//如果该字段有tag标签就显示,否则就不显示
if tagVal != "" {
fmt.Printf("Field %d tag 为=%v\n", i, tagVal)
}
}
//获取到该结构体有多少个方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d methods\n", numOfMethod)
//var params []reflect.Value
//方法的排序默认是按照函数名的排序(ASCII码)
val.Method(1).Call(nil) //获取到第二个【下标为1】方法,调用它 【传参为空】
//调用结构体的第1个方法 Method(0)
var params []reflect.Value //声明了 []reflect.Value()
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) //传入的参数是[]reflect.Value,返回[]reflect.Value
fmt.Println("res=", res[0].Int()) //返回结果,返回的结果是[]reflect.Value
}
func main(){
var a Monster = Monster{
Name: "黄鼠狼精",
Age: 400,
Score: 30.9,
}
TestStruct(a)
}
3 网络编程
3.1 概念及前置知识
- TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的. 比如: QQ 聊天
- b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依旧是用 tcp socket 实现的。 比如: 京东商城 【这属于 go web 开发范畴 】
- 计算机间要相互通讯,必须要求网线,网卡,或者是无线网卡.
4. 协议
5. 端口
- 0 号是保留端口.
- 1-1024 是固定端口(程序员不要使用),又叫有名端口,即被某些程序固定使用,一般程序员不使用.
- 常见端口:22: SSH 远程登录协议 23: telnet 使用 21: ftp 使用
25: smtp 服务使用 80: iis 使用 7: echo 服务- 1025-65535 是动态端口
这些端口,程序员可以使用.
注意:
-
在计算机(尤其是做服务器)要尽可能的少开端口
-
一个端口只能被一个程序监听
-
如果使用 netstat –an 可以查看本机有哪些端口在监听
-
可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口
3.2 快速入门
- 服务端的处理流程
- 监听端口 8888
- 接收客户端的 tcp 链接,建立客户端和服务器端的链接.
- 创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
- 客户端的处理流程
- 建立与服务端的链接
- 发送请求数据[终端],接收服务器端返回的结果数据
- 关闭链接
①服务端功能及代码
功能:
- 编写一个服务器端程序,在 8888 端口监听可以和多个客户端创建链接
链接成功后,客户端可以发送数据,服务器端接受数据,并显示在终端上. 先使用 telnet 来测试,然后编写客户端程序来测试
代码:
package main
import (
"fmt"
"net"
)
func process(conn net.Conn) {
//循环接收客户端发送的数据
defer conn.Close()
for {
//创建一个新的切片
buf := make([]byte, 1024)
//1. 等待客户端通过conn发送消息
//2. 如果客户端没有write(发送消息),那么协程就阻塞在这里
fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
n, err := conn.Read(buf) //从conn中读取
if err != nil {
fmt.Printf("客户端退出 err=%v", err)
return
}
//3. 显示客户端给服务端发送的数据(打印在控制台上)
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听...")
//1. tcp表示使用的网络协议是tcp
//2. 0.0.0.0:8888表示在本地监听8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close() //延时关闭listen
//循环等待客户端来连接服务端
for {
//等待客户端连接
fmt.Println("等待客户端来连接...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Printf("Accept() success con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}
//这里准备起一个协程,为客户端服务
go process(conn)
}
}
运行之后效果:
服务器开始监听...
等待客户端来连接...
②客户端功能及代码
- 编写一个客户端端程序,能链接到 服务器端的 8888 端口
- 客户端可以发送单行数据,然后就退出
- 能通过终端输入数据(输入一行发送一行), 并发送给服务器端
- 在终端输入 exit,表示退出程序.
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "192.168.1.100:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 表示标准输入:【终端】
for {
//从终端读取一行用户输入,并发送给服务端
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//功能二:当用户输入exit就退出
line = strings.Trim(line, "\r\n")
if line == "exit" {
fmt.Println("客户端退出...")
break
}
n, err := conn.Write([]byte(line))
if err != nil {
fmt.Println("conn Write err=", err)
}
fmt.Printf("客户端发送了 %d字节的数据\n", n)
}
}
③运行效果
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/148494.html
暂无评论内容