GoAdvance

作者: 雪上霜 | 来源:发表于2020-06-13 16:43 被阅读0次
    image.png

    指针

    指针就是地址,指针变量就是存储地址的变量

    *p : 解引用,间接引用

    栈帧:用来给函数运行提供内存空间,取内存于stack上。

    ​ 当函数调用时,产生栈帧,函数调用结束,释放栈帧。

    ​ 栈存放:局部变量,形参,内存字段描述值(栈基指针与栈顶指针)。

    指针使用注意:

    ​ 空指针:未被初始化的指针。

    ​ 野指针:被一片无效的地址初始化的指针。

    new:在heap上申请一片内存地址空间

    %q:打印go语言格式的字符串

    变量存储:

    ​ 等号左边的变量代表变量所指向的内存空间。(写)

    ​ 等号右边的变量代表变量内存存储的数据值。(读)

    指针的函数传参(传引用):

    ​ 传引用。:将地址值作为函数参数,返回值后传递。

    ​ 传值:将实参的值拷贝一份给形参。

    ​ 传引用:在A栈帧内部,修改B栈帧中的变量值。

    函数传参:值传递。

    切片

    ​ 为什么切片:

            1. 数组的容量固定,不能自动扩展。
            2. 值传递。数组作为函数参数时,将整个数组拷贝一份给形参。
    

    在go语言中,几乎可以使用切片代替数组使用。

    • 切片的本质:

      • 不是一个数组的指针,是一种数据结构体,用了操作数组内部元素。
    • 数组和切片区别:

      • 数组指定长度
      • 切片不需要。
    • 切片的使用:

      • 切片名称[low:high:max]
      • low:起始下标位置
      • high:结束下标位置 len = high - low
      • 容量:cap = max- low。
      • 截取数组,初始化切片时,如果没有指定容量,则容量跟随原数组(切片)容量。
    • 切片创建:

      • slice := []int{1,2,3,4}
      • slice := make([]int,长度,容量)
        • slice := make([]int,长度) 创建切片时,没有指定容量,容量==长度
    • 切片做函数参数--传引用。

    • append:在切片末尾添加

      • append(切片对象,待追加元素)
      • 向切片增加元素时,切片的容量会自动增长,
    • copy:

      • copy(目标位置切片,源切片)
      • 拷贝过程中,直接位置拷贝。

    map

    • 字典、映射:key-value ,key唯一且无序,不能为引用类型或包含引用类型的结构。

    • map不能使用cap。

    • cap支持:array、slice、channel

    • 创建方式:

      var m1 map[int]string //此种声明为nil,没有空间不能直接存储key-value
      m2 := map[int]string{}    //可以存放,长度为0,
      m3 := make(map[int]string)    //没有指定长度,默认len=0
      m4 := make(map[int]string,5)  //指定长度len=5
      
    • 初始化:

      • var m map[int]string = map[int]string{1:"ivn",2:"osenc"}    //key不能重复
        m2 := map[int]string{1:"ivn",2:"osenc"} //key不能重复
        
    • 赋值:赋值过程中,如果map的key与之前的相同,则替换,不同的添加。

      • m3 := make(map[int]string,1)
        m3[100] = "string"
        m3[3] = "hello"
        m3[3] = "world"     //将之前key为3的替换。
        
    • 遍历

      • for k,v := range m{
            fmt.Printf("key:%d----value:%d\n",k,v)
        }
        for k:= range m{
            fmt.Printf("key:%d\n",k)
        }
        
    • 判断

      • var m map[int]string = map[int]string{1:"ivn",2:"osenc"}
        if v,has := m[1];has{
            fmt.Println("value:",v,"has:",has)
        }else{
            fmt.Println("value:",v,"has:",has)
        }
        //返回value,bool是否存在?
        
    • 做函数参数、返回值,传引用。
      删除失败则什么也不做。

      • func mapDelete(m map[int]string,key int){
            delete(m,key)
        }
        
        func main(){
            var m map[int]string = map[int]string{1:"ivn",2:"osenc"}
            mapDelete(m,2)
            fmt.Println(m)
        }
        
    image.png
    package main
    
    import(
        "fmt"
        "strings"
    )
    
    func wordCountFunc(str string)map[string]int{
        s := strings.Fields(str)    //将字符串拆分为字符串切片
        m := make(map[string]int)   //创建一个用于存储word次数map
        for ,v := range s{
            if _,has := m[v];has{
                m[v]++
            }else{
                m[v] = 1
            }
        }
        return m
    }
    
    func main(){
        str := "I lov my work and I love my family too"
        
        mret := wordCountFunc(str)  
        
        for k,v := range mret{
            fmt.Println(k + ":" + v)
        }
    }
    

    结构体

    是一种数据类型,类型定义

    内部的成员变量不能赋值,只能用这种定义方式,不能使用var name string 定义内部变量。

    type Person struct{
    
        name string
    
        sex byte
    
        age int
    
    }
    
    • 普通变量定义和初始化

      • 顺序初始化,依次将结构体内部的成员初始化。

        • var man Person = Person{"andy",'m',20}
          
      • 指定初始化成员:

        • var man Person = Person{name:"andy"}
          
    • 普通变量的赋值和使用:

      • 使用“."索引成员变量

        • man.name = "andy"
          
    • 结构体变量的比较和赋值:

      • 比较:只能使用==和!= 不能使用> < >= <=
      • 相同结构体(成员变量的顺序、内容相同)之间可以直接赋值
    • 结构体传参:

      • 讲结构体变量的值拷贝一份,传递。 --几乎不用,内存消耗大,效率低
    • 结构体地址:

      • 结构体变量的地址==结构体首个元素的地址。

      • unsafe.Sizeof(变量名)-->求此种类型变量的占用内存大小。
        string在结构体中类型一般会16字节,int一般在64位系统下会自动占8个字节,
        这两个组成的结构体,虽然16比8大,但是依然是以8字节对齐的,这与内存的对齐结构、CPU、数据总线等有关,以8字节对齐,这种结构体所占内存大小为8
        

      指针与结构体

    • 指针遍历的定义和初始化:

      • 顺序初始化:依次将结构体内部所有成员初始化

        • var man *Person = &Person{"andy",'m',20}
          
      • new(Person)

        • var man *Person
          man = new(Person)
          
    • 指针索引成员变量:

      • 使用“."索引成员变量

        • man.name = "andy"
          
    • 结构体地址:

      • 结构体变量的值==结构体首个元素的地址。
    image.png
    • 结构体指针传参:

      • unsafe.Sizeof(指针)-->不管何种类型的指针,64位系统下,大小一直,均为8字节。
        讲结构体变量地址值传递,传引用--效率高,节省空间。
        
    image.png
    image.png
    • 结构体指针做函数返回值:
      • 不能返回局部变量的地址值。 -- 局部变量保存在栈帧上,函数调用结束后,栈帧是否,局部变量的地址,不能受系统的保护,随时可能分配给其他程序。-->个人认为这一条有歧义,因为有可能会发生逃逸。
      • 可以返回局部变量的值。

    字符串

    strings.Split(str,sep)  //str按sep拆分
    strings.Fields(str) //str按空格拆分
    strings.HasSuffix(str,sep)  //判断str是否sep结束标记
    strings.HasPrefix(str,sep)  //判断str是否sep开始标记
    

    文件

    • 创建文件Create: 文件不存在后缀,文件存在,将文件内容清空。

      • 参数:name,打开文件的路径:绝对路径、相对路径
    • 打开文件 Open: 以只读方式打开文件。文件不存在,打开失败

      • 参数name:打开文件的路径:绝对路径、相对路径
    • 打开文件OpenFile: 以只读、只写、读写方式打开文件。

      • 参数 name:打开文件的路径:绝对路径、相对路径
      • 参数打开文件权限:O_RDONLY,O_WRONLY,O_RDWR
      • 参数3:一般传6
    • 写文件:

      • 按字符串写:WriteString() 返回n个写入的字符,
        • 回车换行:win:\r\n Linux:\n
      • 按位置写:Seek():修改文件的读写指针位置
        • 参1:偏移量,正数:向文件尾偏,负:向文件头偏。
        • 参2:偏移起始位置
          • io.SeekStart:文件起始位置
          • io.SeekCurrent:文件当前位置
          • io.SeekEnd:文件结束位置
        • 返回值:从起始位置,到当前文件读写指针位置的偏移量。
      • 按字节写:WriteAt():在文件指定偏移位置,写入[]byte,通常搭配Seek()
        • 参1:[]byte:要写入的数据
        • 参2:偏移量。
        • 返回值:实际写入的字节数。
    • 读文件:

      • 按行读:buf,err := bufio.NewReader(f).ReadBytes('\n') fmt.Println(string(buf))]
        • 创建一个带有(用户)缓冲区的Reader(读写器)
          • reader := bufio.NewReader(打开文件的指针)
        • 从reader的缓冲区中,读取指定长度的数据,数据长度取决于参数dlime
          • buf,err := reader.ReadBytes('\n') 按行读
          • 判断到达文件结尾:if err != nil && err == io.EOF 到文件结尾。
          • 文件结束标记,是单独读一次获取到的。
    • 按字节读、写文件

        - read():按字节读文件
        - write():按字节写文件
      
    image.png
    image.png
    创建一个读文件,一个写文件,读多少,写多少即可。
    - buf := make([]byte,4096)  //创建缓冲区
    
    - for {
    
    - n,err := f_r.Read(buf)
    
    - if err != nil && err == io.EOF{
    
      fmt.Println("over")
    
      return
    
    - }
    
    - f_w.Write(buf[:n])   //读多少写多少
    
    - }
    
    
    

    目录操作:

    • 打开目录:OpenFile

      • 参1:路径
      • 参2:只读方式os.O_RDONLY
      • 参3:权限os.ModeDir
      • 返回值:返回一个可以读写目录的文件指针。
    • 读目录:ReadDir

      • 参数:n:几个目录项。-1读取全部目录项。

      • f,err := os.OpenFile(path,os.O_RDONLY,os.ModeDir)
        if err != nil{
            return
        }
        defer f.Close()
        
        info,err := f.ReadDir(-1)
        
        for _,fileinfo := range info{
            if fileinfo.IsDir(){
                fmt.Println(fileInfo.Name(),"是目录")
            }else{
                fmt.Println(fileInfo.Name(),"是文件")
            }
        }
        
        if strings.HasSuffix(fileInfo.Name(),".jpg"){   //判断文件是否以jpg结尾的文件
        
        }
        
      • 返回值:fileinfo切片。

    image.png
    • 参考代码:
    package main
    
    import (
        "bufio"
        "fmt"
        "io"
        "os"
        "strings"
    )
    
    func OpenDir(path *string) *os.File{
        f,err := os.OpenFile(*path,os.O_RDONLY,os.ModeDir)
        if err != nil{
            fmt.Println("打开目录失败")
            return nil
        }
    
        return f
    }
    
    func FindFile(f *os.File,rep string)(str []string){
        info,err := f.Readdir(-1)
        if err != nil{
            fmt.Println("读取目录失败")
            return nil
        }
        for _,fileinfo := range info{
            if strings.HasSuffix(fileinfo.Name(),rep){
                str = append(str,fileinfo.Name())
            }
        }
        return str
    }
    
    func Read(str []string)(lstr []string){
        for _,s := range str{
            f,err := os.Open(s)
            if err != nil{
                fmt.Println("打开文件"+s+"失败")
                return
            }
            defer f.Close()
    
            buf := bufio.NewReader(f)
            for{
                by,err := buf.ReadBytes('\n')   //byte切片输出什么?
                if err != nil && err == io.EOF{
                    fmt.Println("读取文件"+s+"结束")
                    break
                }else if err != nil{
                    fmt.Println("读取文件"+s+"失败")
                    return
                }
                lstr = append(lstr,strings.Fields(string(by))...)
            }
        }
        return
    }
    
    func main(){
        //打开指定目录
        var path string
        fmt.Scanln(&path)
        fd := OpenDir(&path)
        defer fd.Close()
        //D:/code/ubuntu_code/Go/src/github.com/cold-rivers-snow/study/GoAdvance/01ptr/
    
        //找到指定的文件
        var rep string
        fmt.Scanln(&rep)
        str := FindFile(fd,rep)
    
        //打开文件并读取内容
        lstr := Read(str)
    
        var count int = 0
        for _,v := range lstr {
            if v == "return"{
                count++
            }
        }
    
        fmt.Println(count)
    }
    
    

    1s = 1000ms

    1ms = 1000us

    1us = 1000ns

    • 并行:借助多核CPU实现。

    • 并发:

      • 宏观:用户体验上,程序在并行执行。
      • 微观:多个计划任务,顺序执行。在飞快的切换,轮换使用CPU时间轮片。


        image.png
    • 进程并发:

      • 程序:编译成功得到的二进制文件。 占用磁盘空间。 死的 1
      • 进程:运行起来程序,占用系统资源(内存) 活的 N
    image.png
    • 进程的状态:初始态、就绪态、运行态、阻塞态、终止态。

    • 线程并发:

      • 线程:LWP轻量级的进程,最小的执行单位。-- CPU分配时间轮片的对象
      • 进程:最小的系统资源分配单位。
    • 同步:

      • 协同步调,规划先后顺序。
    • 线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回,同时其他线程为保证数据一致性,不能调用该功能。

      • 线程同步机制:
        • 互斥锁(互斥量):建议锁,拿到锁后,才能访问数据,没有拿到锁的线程,阻塞等待,等到拿锁的线程释放锁。
        • 读写锁:一把锁(读属性、写属性)。写独占,读共享。写锁优先级高。
    • 协程并发:

      • 协程:coroutine。轻量级的线程。
      • Python、Lua、Rust ......新兴语言有。
      • 提高程序执行效率。
    • 总结:

      • 进程、线程、协程都可以完成并发。
      • 进程:稳定性强
      • 线程:节省资源
      • 协程:效率高
    • go程:

      • image.png
    • go程:

      • 创建:
        • 创建于进程中,直接使用go关键字,放置于函数调用签名,产生一个go程,并发。
      • 特性:
        • 主goroutine结束,子goroutine随之退出。
    • runtime包:

      • runtime.Gosched():出让当前go程所占用的CPU时间片。当再次获得CPU时,从出让位置继续恢复执行。

      • func main(){
            go func(){
                for i:= 0;i < 10;i++{
                    fmt.Println("this is groutine test")
                    //time.Sleep(100*time.Microsecond)
                }
            }()
        
            for{
                runtime.Gosched()   //出让当前go程所占用CPU时间片
                fmt.Println("this is main test")
                //time.Sleep(100*time.Microsecond)
            }
        }
        
      • runtime.Goexit():结束调用改函数的当前go程,Goexit()之前注册的defer都生效。

      • return:返回当前函数调用到调用者那里去。return之前的defer注册。

      • func test(){
            defer fmt.Println("cc")
            return
            //runtime.Goexit()
            fmt.Println("dd")
        }
        func main(){
            go func(){
                defer fmt.Println("aaa")
                test()
                defer fmt.Println("bbb")
            }()
            for{
                ;
            }
        }
        
    • runtime.GOMAXPROCS()设置当前进程使用最大CPU核数。,返回上一次的核数

      • func main(){
            
            n := runtime.GOMAXPROCS(1)  //返回的是上一次的核数。
            fmt.Println(n)
        
            for{
                go fmt.Print(0)     //子go程
                fmt.Print(1)        //主go程
            }
        }
        
    • runtime.GOROOT() 返回go的根目录

    channel

    • 是一种数据类型,对应一个“管道”、(通道FIFO)。

    • 内部实现了同步,确保并发安全。

    • 通过通信来共享内存,而不是共享内存来通信。

    • channel的定义

      • make(chan 在channel中传递的数据类型,容量) 容量=0,无缓冲channel,容量> 0,有缓冲channel

      • make (chan int) make(chan int ,5)

      • //全局定义channel,用了数据同步
        var channel = make(chan int)
        
        func printer(s string){
            for _,ch := range s{
                fmt.Printf("%c",ch)     
                time.Sleep(300 * time.Millisecond)
            }
        }
        
        func person1(){     //person1先执行
            printer("hello")
            channel <- 8
        }
        func person2(){     //person2后执行
            <-channel
            printer("workd")
        }
        func main(){
            go person1()
            go person2()
            for{
                ;
            }
        }
        
      • 补充知识点:

        • 每当一个进程启动,系统会自动打开三个文件:标准输入、标准输出、标准错误 -- 对应stdin(0)、stdout(1)、stderr(2)
        • 当进程运行结束,操作系统自动关闭三个文件。
      • channel有两个端:

        • 一端:写端(传入端) chan <-
        • 另一端:读端(传出端) <- chan
        • 要求:读端和写端必须同时满足条件,才能在chan上进行数据流动,否则,则阻塞。
    • channel同步传递数据

      • func main(){
            ch := make(chan string) //无缓冲channel
            fmt.Println(len(ch),cap(ch))    //len求取剩余未读取数据个数,cap通道容量。
            go func(){
                for i:=0;i < 2;i++{
                    fmt.Println(i)
                }
                //通知go打印
                ch <- "子go打印完毕"
            }()
            
            str := <- ch
            fmt.Println(str)
        }
        
    image.png
    • 无缓冲channel:-- 同步通信

      • 通道容量为0,len = 0
      • channel应用于两个go程中。一个读,一个写。
      • 具备同步的能力,读写同步.
    • 有缓冲channel:-- 异步通信

      • 通道容量为非0,len(ch):channel中剩余未读取的数据个数,cap(ch):通道的容量

      • channel应用于两个go程中。一个读,一个写。

      • 缓冲区可以进行数据存储,存储到容量上限,阻塞,具有异步的能力,不需要同时操作channel缓冲区。

      • func main(){
            ch := make(chan int,3)  //存满3个元素之前不会阻塞
            fmt.Println(len(ch),cap(ch))
            
            go func(){
                for i:=0;i < 8;i++{
                    fmt.Println("子go程",i,len(ch),cap(ch))
                    ch <- i
                }
            }()
            time.Sleep(time.Second*4)
            for i:=0;i < 8;i++{
                num := ch
                fmt.Println("主go程",num,len(ch),cap(ch))
            }
        }
        
      • io输出会有延迟。

    • 关闭channel:

      • 确定不再向对端发送、接收数据。使用close(ch)关闭ch channel

      • 对端可以判断channel是否关闭:

        • if num,ok ;= <- ch;ok == true{
            如果对端已经关闭,ok ->false,num无数据
            如果对端没有关闭,ok ->true,num保存读到的数据。
          }
          
        • func main(){
            ch := make(chan int)
            
            go func(){
                for i:=0;i < 8;i++{
                    ch <- i
                }
                close(ch)   //写端,写完数据主动关闭channel
            }()
            
            //ok方式
            for{
                if num,ok := <- ch;ok == true{
                    fmt.Println("读到数据",true)
                }else{
                    break
                }
            }
            //range方式读取
              for num := range ch{
                  fmt.Println(ch)
              }
          }
          
      • 总结:

        • 数据不发送完,不应该关闭。
        • 已经关闭的channel,不能再向其写数据,报错(panic:send on closed channel),可以读数据,读到的数据为0 -- 写端关闭。无缓冲channel :读到0,有缓冲channel:如果缓冲区内有数据则读数据,读到数据完毕之后,可以继续读,读到0/false/""等默认值。
    • 单向channel:

      • 默认channel是双向的,var ch chan int ch = make(chan int)

      • 单向写channel: var sendCh chan <- int sendCh = make(chan <- int) 不能读

      • 单向读channel: var recvCh <- chan int recvCh = make(<- chan int) 不能写

      • 转换:

        • 双向channel可以饮食转换为任意一种单向channel
          • sendCh = ch
        • 单向channel不能转换为双向channel
          • ch = sendCh/recvCh /error
      • 传参:传引用。

      • func main(){
            ch := make(chan int)    //双向channel
        
            var sendCh chan <- int = ch
            sendCh <- 789
            num:= sendCh
            
            var recvCh <- chan int = ch
            num := <- recvCh 
            
            //反向赋值
            var ch2 chan int = sendCh   //error
        }
        
      • func send(out chan<- int){
            out <- 89
            close(out)
        }
        func recv(in <- chan int){
            n := <- in
            fmt.Println(n)
        }
        func main(){
            ch := make(chan int)
            go func(){
                send(ch)    //双向channel转为写channel   
            }()
            recv(ch)
        }
        

    生产者消费者模型

    • 生产者:发送数据端
    • 消费者:接收数据端
    • 缓冲区:
      • 解耦:降低生产者和消费者之间的耦合度
      • 并发:生产者消费者不对等时,能保持正常通信
      • 缓存:生产者和消费者数据处理速度不一致时,暂存数据
    image.png
    //生产者消费者模型
    
    func producer(out chan<- int){
        for i:=0;i < 10;i++{
            out <- i*i
        }
        close(out)          //不写close会怎样?只有写关闭,才能一次通信完毕。
    }
    
    func consumer(in <-chan int){
        for num := range in{
            fmt.Println("消费者拿到",num)
        }
    }
    
    func main(){
        ch := make(chan int)
        //ch := make(chan int,5)
        
        go producer(ch) //子go程  生产者
        
        consumer(ch)    //主go程 消费者
        
    }
    

    定时器

    • time.Timer:创建定时器,指定定时时长,定时到达后,系统会自动向定时器的成员C写系统当前时间(对chan的写操作)

    • type Timer struct{
        C <- chan Time
        r runtimeTimer
      }
      
    • 读Timer.C得到定时后的系统时间,并且完成一次chan的读操作。

    • func main(){
        fmt.Println("当前时间",time.Now())
        //创建定时器
        myTime := time.NewTimer(time.Second*2)
        nowTime := <- myTime.C  //channel类型
        fmt.Println("现下时间",nowTime)
      }
      
    • 定时方法:time包

      • 第一种:sleep
      • 第二种:Timer
      • time.After
    • fmt.Println("当前时间",time.Now())
      nowTim := <-time.After(time.Second*2)
      fmt.Println(nowTim)
      
    • 定时器的停止和重置

      • func main(){
            myTimer := time.NewTimer(time.Second*2) //创建定时器
            myTimer.Reset(1 * time.Second)  //重置定时器时长
            go func(){
                <- myTimer.C
                fmt.Println("子go程,定时完毕")
            }()
            myTimer.Stop()  //设置定时器停止,归0  再执行<-myTime.C会阻塞
            for{
                ;
            }
        }
        
    • 周期定时:

      • time.Ticker

      • type Ticker struct{
            C <- chan Time
            r runtime.Timer
        }
        
      • func main(){
            quit := make(chan bool)
            fmt.Println(time.Now())
            myTicker := time.NewTicker(time.Second)     //定义一个定时器
            i := 0
            go func(){
                nowTime := <- myTicker.C
                i++
                fmt.Println(nowTime)
                if i == 8{
                    quit <- true    //解除 主go程阻塞
                }
            }()
            <-quit      //子go程 循环获取<-myTicker.C期间,一直阻塞
        }
        

    select

    • 可以监听channel上的数据流动,读、写。

    • case语句必须是IO操作,不能任意写判别表达式

    • 按照顺序从头到尾评估每一个发送和接收的数据,如果任意一个语句可以继续执行,则从可执行的语句中任意选择一条来使用。

    • func main(){
        ch := make(chan int)    //用来进行数据通信的channel
        quit := make(chan bool) //用来判断是否退出的channel
        //ch2 := make(chan string)
        
        go func(){              //写数据
            for i:= 0;i < 5;i++{
                ch <- i
                time.Sleep(time.Second)
            }
            close(ch)
            quit<-true      //通知主go程退出
            runtime.Goexit()
          }()
        
        for{                //主go程 读数据
            select{
                case num := <- ch:
                    fmt.Println(num)
                case <- quit:
                    return  //终止进程
            }
            fmt.Println("------------------")
        }
      
      }
      
    • select判断不能分两步,判断的时候直接拿出来。

    • 注意事项:

      • 监听case中,没有满足的监听条件,阻塞
      • 监听的case中,有多个满足监听条件,任选一个执行。
      • 可以使用default来出来所以case都不满足监听条件的状态,通常不用(会产生忙轮询)
      • select自身不带有循环机制,需借助for来循环监听
      • break只能跳出select中额一个case选项,类似于switch中的用法。
    • select实现fibonacci数列:

      • func fibonacci(ch <-chan int,quit <-chan bool){
            for {
                select{
                    case num := <-ch:
                        fmt.Println(num)
                    case <-quit:
                        //return
                        runtime.Goexit()    //等效于return
                }
            }
        }
        func main(){
            ch := make(chan int)
            quit := make(chan bool)
            
            go fibonacci(ch,quit)   //子go程,打印channel
            
            x,y := 1,1
            for i := 0;i < 20;i++{
                ch <- x
                x,y = y,x + y
            }
            quit <- true
        }
        
    • select超时

      • func main(){
            ch := make(chan int)
            quit := make(chan bool)
            
            go func(){      //子go程获取数据
                for{
                    select{
                        case num:= <-ch:
                            fmt.Println(num)
                        case <-time.After(3*time.Second)
                            quit<-true
                            return
                            //runtime.Goexit()
                    }
                }
            }()
            for i:=0;i <2;i++{
                ch <- i
                time.Sleep(2*time.Second)
            }
            
            <- quit
            fmt.Println("finish")
        }
        
      • goto 的标签必须在本函数内。

    死锁

    • 单go程自己死锁

      • channel应该至少2个及以上的go程中通信

        //死锁版
        func main(){
            ch := make(chan int)
            ch <- 789
            num := <-ch
            fmt.Println("num:",num)
        }
        
      • //正确版
        func main(){
            ch := make(chan int)
            
            num := <-ch
            fmt.Println("num:",num)
            go func(){
                ch <- 789
            }()
        }
        
    • go程间channel访问顺序导致死锁

      • 使用channel一端读(写),要保证另一端(写)读操作,同时有机会执行。否则死锁。

      • //错误版
        func main(){
            ch := make(chan int)
            num := <-ch
            fmt.Println(num)
            go func(){
                ch <- 789
            }()
        }
        
      • //正确版
        func main(){
            ch := make(chan int)
            
            go func(){
                ch <- 789
            }()
            
            num := <-ch
            fmt.Println(num)
        }
        
    • 多go程,多channel交叉死锁。

      • func main(){
            ch1 := make(chan int)
            ch2 := make(chan int)
            
            go func(){      //子
                for{
                    select{
                        case num := <-ch1:
                            ch2 <- num
                    }
                }
            }()
            
            for{
                select{
                    case num := <-ch2:
                        ch1 <- num
                }
            }
        }
        
    • 在go语言中,尽量不要将互斥锁、读写锁与channel混用---隐性死锁

    • 不是一种锁,是一种错误使用锁的现象。

    • 互斥锁

      • //使用channel实现同步
        ch := make(chan int)
        func printer(str string){
            for _,ch := range str{
                fmt.Printf("%c",ch)
                time.Sleep(time.Millisecond*300)
            }
        }
        
        func person1(){
            printer("hello")
            ch <- 98
        }
        
        func person2(){
            <- ch
            printer("world")
        }
        
        func main(){
            go person1()
            go person2()
            
            for{
                ;
            }
        }
        
      • //使用互斥锁实现同步
        var mutex sync.Mutex    //创建一个互斥量,新建的互斥锁状态为0,未加锁状态,锁只有一把
        func printer(str string){
            for _,ch := range str{
                fmt.Printf("%c",ch)
                time.Sleep(time.Millisecond*300)
            }
        }
        
        func person1(){
            mutex.Lock()        //访问数据之前加锁
            printer("hello")
            mutex.Unlock()      //共享数据访问结束解锁。
        }
        
        func person2(){
            printer("world")
        }
        
        func main(){
            go person1()
            go person2()
            
            for{
                ;
            }
        }
        
      • A、B go程共同访问共享数据,由于CPU调度随机,需要对共享数据访问顺序加以限定(同步)。创建mutex(互斥锁),访问共享数据之前,加锁,访问结束,解锁。在Ago程加锁期间,Bgo程加锁失败--阻塞。直到Ago程解锁mutex,B从阻塞处,恢复执行。

    image.png

    读写锁

    • sync.RWMutex

    • 读时共享,写时独占,写锁优先级比读锁高。

    • 产生死锁:

    • var rwMutex sync.RWMutex
      
      func readGo(in <-chan int,idx int){
        for{
            rwMutex.RLock() //读模式加锁
            num := <- in
            fmt.Printf("%dth 读go程,读出:%d\n",idx,num)
            rwMutex.RUnlock()   
        }
      }
      
      func writeGo(out chan<- int,idx int){
        for{
            //生成随机数
            num := rand.Intn(1000)
            rwMutex.Lock()  //写模式加锁
            out <- num
            fmt.Printf("%dth 写go程,写入:%d\n",idx,num)
            time.Sleep(time.Millisecond*300)    //放大实验现象
            rwMutex.Unlock()
        }
      }
      func main(){
        //播种随机数种子
        rand.Send(time.Now().UnixNano())
        //quit := make(chan bool)       //用于关闭主go程channel
        ch := make(chan int)    //用于数据传递channel
        
        for i:=0;i < 8;i++{
            go readGo(ch,i+1)
        }
        
        for i:=0;i < 8;i++{
            go writeGo(ch,i+1)
        }
        
        //<-quit
        
        for{
            ;
        }
      }
      
    • 在go语言中,尽量不要将互斥锁、读写锁与channel混用---隐性死锁


      image.png
    • 读写锁数据同步

      •   var rwMutex sync.RWMutex
          var value int         //定义全局变量模拟共享数据
          
          func readGo(idx int){
            for{
                rwMutex.RLock() //读模式加锁
                num := value
                fmt.Printf("%dth 读go程,读出:%d\n",idx,num)
                rwMutex.RUnlock()   
            }
          }
          
          func writeGo(idx int){
            for{
                //生成随机数
                num := rand.Intn(1000)
                rwMutex.Lock()  //写模式加锁
                value = num
                fmt.Printf("%dth 写go程,写入:%d\n",idx,num)
                time.Sleep(time.Millisecond*300)    //放大实验现象
                rwMutex.Unlock()
            }
          }
          func main(){
            //播种随机数种子
            rand.Send(time.Now().UnixNano())
            
            for i:=0;i < 8;i++{
                go readGo(i+1)
            }
            
            for i:=0;i < 8;i++{
                go writeGo(i+1)
            }
         
            
            for{
                ;
            }
          }
        
      • //channel替换读写锁
        
          
          func readGo(in <-chan int,idx int){
            for{
                num := <-in
                fmt.Printf("%dth 读go程,读出:%d\n",idx,num)
            }
          }
          
          func writeGo(out chan<- int,idx int){
            for{
                //生成随机数
                num := rand.Intn(1000)
                out <- num
                fmt.Printf("%dth 写go程,写入:%d\n",idx,num)
                time.Sleep(time.Millisecond*300)    //放大实验现象
            }
          }
          func main(){
            //播种随机数种子
            rand.Send(time.Now().UnixNano())
            ch := make(chan int)
            
            for i:=0;i < 8;i++{
                go readGo(ch,i+1)
            }
            
            for i:=0;i < 8;i++{
                go writeGo(ch,i+1)
            }
         
            
            for{
                ;
            }
          }
        
    • 条件变量

      • 本身不是锁,但经常与锁结合使用。


        image.png
    • Wait的三点:

      • 阻塞等待条件变量满足
      • 释放已掌握的互斥锁相当于cond.L.Unlock(),注意:两步为一个原子操作。
      • 当被唤醒,Wait函数返回时,解除阻塞并重新获取互斥锁,相当于cond.L.Lock()
    • Signal:唤醒一个go程

    • Broadcast:唤醒所有的go程。

    • 使用流程:

      • 创建条件变量 var cond sync.Cond

      • 指定条件变量用的锁:cond.L = new(sync.Mutex)

      • cond.L.Lock() 给公共区加锁(互斥量)

      • 判断是否到达阻塞条件(缓冲区满/空) for循环判断

        • for len(ch) == cap(ch){
            cond.Wait()     1)阻塞2)解锁3)加锁
          }
          
      • 访问公共区--读、写数据,打印

      • 解锁条件变量用的锁 cond.L.Unlock()

      • 唤醒阻塞在条件变量的对端。Signal,Broadcast

    • //条件变量实现生产者消费者
      //生产者消费者模型
      
      var cond sync.Cond
      
      func producer(out chan<- int,idx int){
        for{
            //先加锁
            cond.L.Lock()
            //判断缓冲区是否满
            for len(out) == 5{
                cond.Wait()
            }
            num := rand.Intn(800)
            out <- num
            fmt.Printf("生产者%dth,生产,%d\n",idx,num)
            //访问公共区结束,释放锁,并打印结果
            cond.L.Unlock()
            //唤醒阻塞在条件变量上的消费者
            cond.Signal()
            time.Sleep(time.Millisecond*300)
        }
      }
      
      func consumer(in <-chan int,idx,int){
          for{
            //先加锁
            cond.L.Lock()
            //判断是否为空
            for len(in) == 0{
                cond.Wait()
            }
            //访问公共区
            num := in
            fmt.Printf("消费者%dth,消费,%d\n",idx,num)
            //访问结束,结束
            cond.L.Unlock()
            //唤醒阻塞的生产者
            cond.Signal()
            time.Sleep(time.Millisecond*300)
          }
      }
      
      func main(){
          ch := make(chan int,5)
          quit := make(chan bool)
          rand.Seed(time.Now().UnixNano())
          
          for i:=0;i < 8;i++{
            go producer(ch,i+1) //子go程  生产者
        }
          
          for i:=0;i < 8;i++{
            go consumer(ch,i+1)    //消费者
        }
          
          <- quit
          
      }
      

    网络编程

    • 协议:一组规则,要求使用协议的双方,要严格的遵守的协议内容

    • 网络分层结构:

      • OSI七层模型:
      • TCP/IP四层模型:
      • 链路层:设备
      • 网络层:IP
      • 传输层:port
      • 应用层:应用程序
    • 各层功能:

      • 链路层:ARP
        • 源Mac----目标Mac
        • ARP协议作用:借助IP获取Mac地址。


          image.png
    • 网络层:IP

      • 源IP--目标IP
      • IP协议作用:在网络中唯一标识一台主机。
      • IP地址本质:2进制数。---点分十进制IP地址(string)
    • 传输层TCP/UDP

      • port --- 在一台主机上唯一标识一个进程。
    • 应用层:ftp、http、自定义

      • 对数据进行封装、解封装。
    • 数据通信过程:

      • 封装:应用层---传输层--网络层---链路层,没有经过封装不能再网络中传递。
      • 解封装:链路层--网络层--传输层--应用层


        image.png

        总结通信过程:

    • MAC地址:不需要用户指定, (ARP协议)IP--->MAC

    • IP地址:需要用户指定 --- 确定主机

    • port端口号(需要用户指定) --确定程序

      • 1、不能用的系统占用的默认端口。 5000+端口 (8080除外)
      • 2、65535为上限

    socket编程

    • 网络通信过程中,socket一定是成对出现的。


      image.png
      image.png
    • 一共三个socket,listen、accept、dial等返回值
    • 通信的两个socket,accept,dial的返回值,listen的socket用于设定IP和port。
    image.png
    • C/S

      • 优点:数据传输效率高,协议选择灵活
      • 缺点:工作量大、安全性构成威胁
      • B/S:
        • 优点:开发工作较小,不受平台限制,安全威胁小。
        • 缺点:缓存数据差,协议选择不灵活。
    • TCP-C/S服务器

    • 1、创建监听socket listener:= net.Listen("tcp","ip+port") ip+port --- 服务器自己的IP和port

    • 2、启动监听 conn := listener.Accept() conn用于通信的socket

    • 3、conn.Read()

    • 4、处理使用数据

    • 5、conn.Write()

    • 6、关闭listener,conn

    • server.go
      func main(){
          //指定服务器通信协议,IP和port,创建一个监听的socket
          listener,err := net.Listen("tcp","127.0.0.1:8000")
          if err != nil{
              fmt.Println("net Listen err",err)
              return
          }
          defer listener.Close()
          fmt.Println("服务器等等客户端连接")
          //阻塞监听客户端连接请求
          //用于返回通信socket
          conn,err := listener.Accept()
          if err != nil{
              fmt.Println("listener.Accept()err",err)
              return
          }
          defer conn.Close()
          fmt.Println("服务器与客户端建立连接成功")
          //读取客户端发送的数据
          buf := make([]byte,4096)
          n,err := conn.Read(buf)
          if err != nil{
              fmt.Println("conn.Read err ",err)
              return
          }
          conn.Write(buf[:n])     //读多少,写多少
          //处理数据-- 打印
          fmt.Println("服务器读到数据:",string(buf[:n]))
          
      }
      
    • client.go
      func main(){
          //指定服务器IP+port创建 通信套接字
          conn,err := net.Dial("tcp","127.0.0.1:8000")
          if err != nil{
              fmt.Println("net.Dial err ",err)
              return
          }
          defer conn.Close()
          
          //主动写数据
          conn.Write([]byte("Are you OK"))
          
          buf := make([]byte,4096)
          //接收服务器的数据
          n,err := conn.Read(buf)
          if err != nil{
              fmt.Println("conn.Read err",err)
              return
          }
          fmt.Println("服务器回发",string(buf[:n]))
      }
      
    • TCP-C/S客户端:

    • conn,err := net.Dial("TCP",服务器的IP和port)

    • 写数据给服务器conn.Write()

    • 读服务器回发的数据conn.Read()

    • conn.Close()

    • TCP并发C/S服务器

      • 1、创建监听套接字 listener := net.Listen("tcp",服务器的ip+port) //tcp 不能大写

      • 2、defer listener.Close()

      • 3、for循环阻塞监听客户端连接事件 conn := listener.Accept()

      • 4、创建go程对应每一个客户端进行数据通信 go HandlerConnet()

      • 5、实现HandlerConnet(conn net.Conn)

        • ​ 1、defer conn.Close()
        • 2、获取成功连接的客户端Address conn.RemoteAddr()
        • 3、for 循环读取客户端发送数据 conn.Read(buf)
        • 4、处理数据 小--大 strings.ToUpper()
        • 5、回写转化后的数据 conn.Write([]byte(buf[:n]))
      • func HandlerConnect(conn net.Conn){
            defer conn.Close()
            
            //获取连接的客户端Address
            addr := conn.RemoteAddr()
            fmt.Println(addr,"客户端成功连接!")
            
            //魂环读取客户端发送的数据
            buf := make([]byte,4096)
            for{
                n,err := conn.Read(buf)
                //测试一下接收到的数据。
                //fmt.Println(buf[:n])
                if "exit\n" == string(buf[:n]) || "exit\r\n" == string(buf[:n]){
                    fmt.Println("服务器的客户端退出请求,服务器关闭")
                    return 
                }
                if n == 0{
                    fmt.Println("服务器检测客户端已关闭,断开连接!!!")
                    return
                }
                if err != nil{
                    fmt.Println("conn.Read err ",err)
                    return
                }
                fmt.Println("服务器读到数据:",string(buf[:n]))
                
                //小写转大写回传客户端
                conn.Write([]byte(string.ToUpper(string(buf[:n]))))
            }
        }
        
        func main(){
            //创建监听套接字
            listener,err := net.Listen("tcp","127.0.0.1:8000")
            if err != nil{
                fmt.Println("net.Listen err ",err)
                return
            }
            defer listener.Close()
            
            //监听客户端请求
            for{
                fmt.Println("服务器等待连接:")
                conn,err := listener.Accept()
                if err != nil{
                    fmt.Println("listener.Accept err ",err)
                    return
                }
                
                //具体的完成服务器与客户端数据通信
                go HandlerConnect(conn)
            }
        }
        
    • 服务器判断关闭:

      • Read客户端/服务器返回0,则客户端关闭。(channel关闭时,关闭写channel后,读到的是默认值)
      • 使用nc命令发送数据,末尾会自带\n。
    • TCP-C/S并发客户端

      • func main(){
            //主动发起连接请求
            conn,err := net.Dial("tcp","127.0.0.1:8000")
            if err != nil{
                fmt.Println("net.Dial err ",err)
                return
            }
            defer conn.Close()
            
            //获取用户键盘书输入stdin,将输入数据发送给服务器
            go func(){
                str := make([]byte,4096)
                for{
                    n,err := os.Stdin.Read(str)
                    if err != nil{
                        fmt.Println("os.Stdin err ",err)
                        continue
                    }
                    //写给服务器,读多少写多少
                    conn.Write(str[:n])
                }   
            }()
            //回显服务器回发的数据
            buf := make([]byte,4096)
            for{
                n,err := conn.Read(buf)
                if n == 0{
                    fmt.Println("检测到服务器关闭,客户端也关闭")
                    return
                }
                if err != nil{
                    fmt.Println("conn Read err",err)
                    return
                }
                fmt.Println("客户端读到服务器数据",string(buf[:n]))
            }
        }
        
    image.png
    image.png
    image.png

    TCP通信过程:

    • 三次握手:
      • 1、主动发起请求端:发送SYN
      • 2、被动建立连接请求端:应答ACK同时发送SYN
      • 3、主动发起请求端,发送应答ACK
      • 标志TCP三次握手建立完成---server:Accept()返回, --- client:Dial()返回
    • 四次挥手
      • 1、主动关闭连接请求端,发送FIN
      • 2、被动关闭连接请求端,发送ACK 。标志半关闭完成---close()
      • 3、被动发送连接请求端,发送FIN
      • 4、主动关闭连接请求端,应答ACK 标志:四次挥手完成--close().
    image.png

    TCP状态转换图:

    • 主动发起连接请求端:CLOSED--完成三次握手--ESTABLISEHED(数据通信状态)

    • 被动发起连接请求端:CLOSED--调用Accept函数--LISTEN--完成三次握手--ESTABLISEHED(数据通信状态)--Accept函数返回,数据传递期间---ESTABLISEHED(数据通信状态)

    • 主动关闭连接请求端:

      • ESTABLISEHED--FIN_WAIT_2(半关闭)--TIME_WAIT--2MSL_-- 确认最后一个ACK被对端成功接收--CLOSE
      • 半关闭、TIME_WAIT、2MSL--只会出现在“主动关闭连接请求端”
    • 被动关闭连接请求端:ESTABLISEHED--CLOSE

    • 利用netstat查看网络状态。netstat -an

    • windows:netstat -an | findstr 8001

    • Linux:netstat -an|grep 8001

    • TCP通信:

      • 面向连接的,可靠的数据包传输
    • UDP通信:

      • 无连接的,不可靠的报文传递。


        image.png

        UDP服务器:

    • func main(){
        //组织一个udp地址结构,指定服务器的IP+port
        srcAddr,err := net.ResolveUDPAddr("udp",127.0.0.1:8000)
        if err != nil{
            fmt.Println("ResolceUDPAddr err",err)
            return
        }
        fmt.Println("udp服务器地址结构,创建完成")
      
        //创建用于通信的socket
        udpConn,err := net.ListenUDP("udp",srcAddr)
        if err != nil{
            fmt.Println("net.ListenUDP err",err)
            return
        } 
        defer udpConn.Close()
        fmt.Println("服务器socket创建完成")
        
        //读取客户端发送的数据
        buf := make([]byte,4096)
        //返回3个值,分别是读到的字节数,客户端地址,error
        n,cltAddr,err := udpConn.ReadFromUDP(buf)
        if err != nil{
            fmt.Println("udpConn.ReadFromUDP err",err)
            return
        }
        //模拟出来数据
        fmt.Printf("服务器读到%v的数据:%s\n",cltAddr,string(buf[:n]))
        //回写数据给客户端
        daytime := time.Now().String()
        
        _,err := udpConn.WriteToUDP([]byte(daytime),cltAddr)
        if err != nil{
            fmt.Println("udpConn.Write err",err)
            return
        }
      }
      
    • client.go
      func main(){
        conn,err := net.Dial("udp","127.0.0.1:8000")
          if err != nil{
              fmt.Println("net.Dial err ",err)
              return
          }
          defer conn.Close()
          
          //主动写数据
          conn.Write([]byte("Are you OK"))
          
          buf := make([]byte,4096)
          //接收服务器的数据
          n,err := conn.Read(buf)
          if err != nil{
              fmt.Println("conn.Read err",err)
              return
          }
          fmt.Println("服务器回发",string(buf[:n]))
      }
      
    • UDP服务器:

      • 1、创建server端地址结构(IP+port) net.ResolveUDPAddr()
      • 2、创建用于通信的socket,绑定地址结构 udpConn = net.ListenUDP()
      • 3、defer udpConn.Close()
      • 4、读取客户端发送数 ReadFromUDP()返回 n,cltAddr(客户端的IP+Port),err
      • 5、写数据给客户端 WriteToUDP("待写数据",客户端地址)
    • UDP客户端:

      • 参考TCP客户端
      • net.Dial("udp",server的IP+port)
    • UDP并发服务器

      • func main(){
            //组织一个udp地址结构,指定服务器的IP+port
          srcAddr,err := net.ResolveUDPAddr("udp",127.0.0.1:8000)
          if err != nil{
              fmt.Println("ResolceUDPAddr err",err)
              return
          }
          fmt.Println("udp服务器地址结构,创建完成")
        
          //创建用于通信的socket
          udpConn,err := net.ListenUDP("udp",srcAddr)
          if err != nil{
              fmt.Println("net.ListenUDP err",err)
              return
          } 
          defer udpConn.Close()
          fmt.Println("服务器socket创建完成")
          
          for{
             //读取客户端发送的数据
             buf := make([]byte,4096)
             //返回3个值,分别是读到的字节数,客户端地址,error
             n,cltAddr,err := udpConn.ReadFromUDP(buf)
             if err != nil{
                 fmt.Println("udpConn.ReadFromUDP err",err)
                 return
             }
            //模拟出来数据
             fmt.Printf("服务器读到%v的数据:%s\n",cltAddr,string(buf[:n]))
             
             go func(){
                //回写数据给客户端
                 daytime := time.Now().String()+'\ns'
        
                 _,err := udpConn.WriteToUDP([]byte(daytime),cltAddr)
                 if err != nil{
                     fmt.Println("udpConn.Write err",err)
                     return
                  }
             }()
          }
          
        }
        
    • UDP并发客户端

      • func main(){
          conn,err := net.Dial("udp","127.0.0.1:8000")
            if err != nil{
                fmt.Println("net.Dial err ",err)
                return
            }
            defer conn.Close()
            
            for i:= 0;i < 1000;i++{
                //主动写数据
                conn.Write([]byte("Are you OK"))
        
                buf := make([]byte,4096)
                //接收服务器的数据
                n,err := conn.Read(buf)
                if err != nil{
                    fmt.Println("conn.Read err",err)
                    return
                }
                fmt.Println("服务器回发",string(buf[:n]))
                time.Sleep(time.Second)
            }
        }
        
    • UDP服务器并发:

      • 1、UDP默认支持客户端并发访问
      • 2、使用go程将服务器出了ReadFromUDP 和WriteToUDP操作分开,提高并发效率。


        image.png
        image.png
    • 使用场景:

      • TCP:对数据传输安全性、稳定性要求较高的场合。网络文件传输。下载,上传。
      • UDP:对数据的实时传输要求较高的场合,视频直播,在线电话会议,游戏。
    • 优点:

      • TCP:稳定、安全、有序
      • UDP:效率高、开销小、开发复杂度低
    • 缺点:

      • TCP:效率低,开销大、开发复杂度高。
      • UDP:稳定性差、安全低、无序。

    文件传输

    image.png

    文件属性

    • os.Args  //获取命令行参数,在main函数启动时, 向整个程序传参
      
    • go run xx.go argv1 argv2 argv3 argv4

    • 获取文件属性

      • fileInfo: os.Stat(文件访问绝对路径)
      • fileInfo 接口,两个接口
        • Name()获取文件名
        • Size()获取文件大小
    • func main(){
        list := os.Args
        if len(list) != 2{
            fmt.Println("格式为go run xx.go 文件名")
            return
        }
        
        //提取文件名
        path := list[1]
        //获取文件属性
        fileInfo,err := os.Stat(path)
        if err != nil{
            fmt.Println("os.Stat err",err)
            return
        }
        fmt.Println("文件名:",fileInfo.Name())
        fmt.Println("文件大小",fileInfo.Size())
      }
      
    image.png
    //发送端
    package main
    
    import (
        "fmt"
        "io"
        "net"
        "os"
    )
    
    func main(){
            list := os.Args //获取命令行参数
            if len(list) != 2{
                    fmt.Println("格式为:go run xx.go 文件绝对路径")
                    return
            }
    
            //提取文件绝对路径
            filePath := list[1]
    
            //提取文件名
            fileInfo,err := os.Stat(filePath)
            if err != nil{
                    fmt.Println("os.Stat err",err)
                    return
            }
    
            fileName := fileInfo.Name()
    
            //主动发起连接请求
            conn,err := net.Dial("tcp","127.0.0.1:8000")
            if err != nil{
                fmt.Println("net.Dial err",err)
                return
            }
            defer conn.Close()
    
            //发送文件名给接收到
            conn.Write([]byte(fileName))
    
            //读取服务器回发
            buf := make([]byte,4096)
            n,err := conn.Read(buf)
            if err != nil{
                fmt.Println("net.Read err",err)
                return
            }
    
            if "ok" == string(buf[:n]){
                //写文件内容给服务器
                sendFile(conn,filePath)
            }
    }
    
    func sendFile(conn net.Conn, filePath string) {
        //只读打开文件
        f,err := os.Open(filePath)
        if err != nil{
            fmt.Println("os.Open err",err)
            return
        }
        defer f.Close()
    
        //从本地文件中,读数据写个网络接收端,读多少,写多少,原封不动
        buf := make([]byte,4096)
        for{
            n,err := f.Read(buf)
            if err != nil{
                if err == io.EOF{
                        fmt.Println("发送文件成功")
                }else{
                    fmt.Println("发送 err",err)
                }
                return
            }
            //写到网络socket中
            _,err = conn.Write(buf[:n])
            if err != nil{
                fmt.Println("conn.Write err",err)
                return
            }
        }
    }
    
    //接收端
    package main
    
    import (
        "fmt"
        "net"
        "os"
    )
    
    func main(){
        //创建用于监听的socket
        listener,err := net.Listen("tcp","127.0.0.1:8000")
        if err != nil{
            fmt.Println("net.Listen,err",err)
            return
        }
        defer listener.Close()
    
        //阻塞监听
        conn,err := listener.Accept()
        if err != nil{
            fmt.Println("Listener.Accept,err",err)
            return
        }
        defer conn.Close()
    
        //获取文件名,保存
        buf := make([]byte,4096)
        n,err := conn.Read(buf)
        if err != nil{
            fmt.Println("conn.Read,err",err)
            return
        }
        
        fileName := string(buf[:n])
        
        //回写ok给发送端
        conn.Write([]byte("ok"))
        
        //获取文件内容
        recvFile(conn,fileName)
    }
    
    func recvFile(conn net.Conn, name string) {
        //按照文件名创建文件
        f,err := os.Create(name)
        if err != nil{
            fmt.Println("os.Create,err",err)
            return
        }
        defer f.Close()
    
        //从网络中读数据,写入本地文件
        buf := make([]byte,4096)
        for{
            n,_ := conn.Read(buf)
            if n == 0{
                fmt.Println("接收文件完成")
                return
            }
    
            //写入本地文件,读多少写多少
            f.Write(buf[:n])
        }
    }
    
    • 文件传输:--发送端(客户端)
      • 1、提示用户使用命令行参数输入文件名,接收文件名filePath(含访问路径)
      • 2、使用os.Stat()获取文件属性,得到文件fileName(去除访问路径)
      • 3、主动发起连接服务器请求,结束时关闭连接。
      • 4、发送文件名到接收端conn.Write()
      • 5、读取接收到回发的确认数据conn.Read()
      • 6、判断是否为“ok",如果是,封装函数SendFile()发送文件内容,传参filePath和conn
      • 7、只读Open文件,结束时Close文件
      • 8、循环读本地文件,读到EOF,读取完毕
      • 9、将读到的内容原封不动conn.Write给接收到(服务器)
    • 文件传输--接收到(服务器)
      • 1、创建监听listener,程序结束时关闭
      • 2、阻塞等待客户端连接conn,程序结束时关闭conn
      • 3、读取客户端发送文件名,保存fileName
      • 4、回发“Ok”
      • 5、封装函数RecvFile接收客户端发送的文件内容,传参fileName和conn
      • 6、按文件名Create文件,结束时close
      • 7、循环Read发送端网络文件内容,读到0说明文件读取结束
      • 8、将读到的内容原封不动write到创建的文件中。

    并发聊天室

    模块:

    • 主go程(服务器):监听用户请求
    • 处理用户连接go程:HandleConnect:负责新上线用户的存储,用户消息读取、发送、用户改名、下线处理及超时处理。

    为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。

    • 用户消息广播go程:Manager:负责在线用户遍历,用户消息广播发送,需要与HandleConnect go程及用户子go程协作完成

    • go程间数据及通信:map:存储所有登录聊天室的用户信息,key:用户的ip+port。value:client结构体。
      Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
      通道message:协调并发go程间消息的传递。


      image.png
    • 聊天室模块划分:

      • 主go程(服务器):

        • 创建监听socket,for循环Accept()客户端连接--conn。启动go程 HandlerConnet
      • 处理用户连接go程:HandleConnect:

        • 创建用户结构体对象,存入onlineMap。发送用户登录广播、聊天消息。处理查询在线用户、改名、下线、超时退出。(负责新上线用户的存储,用户消息读取、发送、用户改名、下线处理及超时处理。)为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。
      • 用户消息广播go程:Manager:

        • 监听全局channel message,将读到的消息广播给onlineMap中的所有用户。(负责在线用户遍历,用户消息广播发送,需要与HandleConnect go程及用户子go程协作完成)
      • go程间数据及通信:map:存储所有登录聊天室的用户信息,key:用户的ip+port。value:client结构体。
        Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
        通道message:协调并发go程间消息的传递。

      • WriteMsgToClient:

        • 读取每个用户自带channel C上的消息(由Manager发送该消息)。回写给用户。
      • 全局数据模块:

        • 用户结构体Client{C、Name、Addr string}
        • 在线用户列表:onlineMap[string]Client key:客户端IP+Port value:Client
        • 消息通道:message。
    • 广播用户上线:

      • 1、主go程中,创建监听套接字,记得defer
      • 2、for 循环监听客户端连接请求。Accept()
      • 3、有一个客户端连接,创建新go程 处理客户端数据HandlerConnet(conn) defer
      • 4、定义全局结构体类型, C Name Addr
      • 5、创建全局map、channel
      • 6、实现HandlerConnet函数。获取客户端的IP+Port---RemoteAddr()。初始化新用户结构体信息。name == Addr
      • 7、创建Manager管理go程--Accept之间
      • 8、实现Manager。初始化在线用户map。循环读取全局的channel,如果无数据,阻塞,如果有数据,遍历在线用户map,将数据写到用户的C中。
      • 9、将新用户添加到在线用户map中。 Key== IP+Port value= 新用户结构体
      • 10、创建WriteMsgToClient go程,专门给当前用户写数据。 ---- 来源于用户自带的C中。
      • 11、实现WriteMsgToClient(clnt,conn).遍历自带的C读数据,conn.Write到客户端。
      • 12、HandlerConnet中,结束位置,组织用户上线信息。将用户上线信息写到全局channel中--manager的读就被激活,原来一直阻塞。
      • 13、HandlerConnet中,结尾加for{;}

    chatRoom.go

    package main
    
    import (
        "fmt"
        "net"
    )
    
    //创建用户结构体类型
    type Client struct {
        C chan string
        Name string
        Addr string
    }
    
    //创建全局map,存储在线用户
    var onlineMap map[string]Client
    
    //创建全局channel,传递用户消息
    var message = make(chan string)
    
    func main(){
        //创建监听套接字
        listener,err := net.Listen("tcp","127.0.0.1:8000")
        if err != nil{
            fmt.Println("Listen err",err)
            return
        }
        defer listener.Close()
    
        //创建一个管理者go程,管理map和全局channel
        go Manager()
    
        //循环监听客户端连接请求
        for{
            conn,err := listener.Accept()
            if err != nil{
                fmt.Println("Accept err",err)
                return
            }
            //启动go程处理客户端数据请求
            go HandleConnect(conn)
        }
    }
    
    func Manager() {
        //初始化onlineMap
        onlineMap = make(map[string]Client)
    
        //循环从message中读取
        for{
            //监听全局channel中是否有数据,有数据,存储至msg,无数据阻塞
            msg := <-message
    
            //循环发送消息给所有在线用户  
            for _,clnt := range onlineMap{
                clnt.C <- msg
            }
        }
    
    }
    
    func HandleConnect(conn net.Conn) {
        defer conn.Close()
    
        //获取用户网络地址Ip+port
        netAddr := conn.RemoteAddr().String()
    
        //创建新连接用户的结构体,默认用户为IP+Port
        clnt := Client{
            make(chan string),
            netAddr,
            netAddr,
        }
    
        //将新连接用户,添加到在线用户map中
        onlineMap[netAddr] = clnt
    
        //创建专门用来给当前用户发送消息的go程
        go WriteMsgToClient(clnt,conn)
    
        //发送用户上线消息到全局channel中
        message <- "[" + netAddr + "]" + clnt.Name + "login"
    
        //保证不退出
        for{
            ;
        }
    }
    
    func WriteMsgToClient(clnt Client,conn net.Conn){
        //监听用户自带channel上是否有消息。
        for msg := range clnt.C{
            conn.Write([]byte(msg + "\n"))
        }
    }
    
    • 用户广播消息:
        1. 封装函数MakeMsg()来处理广播、用户信息
        1. HandlerConnet中,创建匿名go程,读取用户socket上发送来的聊天内容。写到全局channel。
        2. for 循环 conn.Read n == 0 err != nil
        3. 写给全局message -- 后续的事,原来广播用户上线模块 完成。(Manager、WriteMsgToClient)
    • 查询在线用户:
        1. 将读取到的用户消息msg结尾的"\n"去掉。
        2. 判断是否是“who”命令
        3. 如果是:遍历在线用户列表,组织显示用户信息。写到socket中。
        4. 如果不是,写给全局message。
    • 修改用户名:
        1. 将读取到的用户消息msg判断是否包含“rename"
        2. 提取“|”后面的字符串,存入到Client的Name成员中
        3. 更新在线用户列表。onlineMap。key -- IP+Port
        4. 提示用户更新完成。conn.Write()
    • 用户退出:
        1. 在用户成功登陆之后,创建监听用户退出的channel ---- isQuit
        2. 当conn.Read == 0, isQuit<- true
        3. 在HandlerConnet结尾for中,添加select监听<-isQuit的读事件
        4. 条件满足,将用户从在线列表移除。组织用户下线消息,写入message(广播)
    • 超时强踢:
        1. 在select中监听定时器,(time.After())计时到达。将用户从在线列表移除。组织用户下线消息,写入message(广播)
        2. 创建监听用户活跃的channel---hasData。
        3. 只用户执行:聊天、改名、who任意一个操作hasData <- true
        4. 在select中添加监听<- hasData.条件满足。不做任何事情。目的是重置计时器。
    package main
    
    import (
        "fmt"
        "net"
        "strings"
        "time"
    )
    
    //创建用户结构体类型
    type Client struct {
        C chan string
        Name string
        Addr string
    }
    
    //创建全局map,存储在线用户
    var onlineMap map[string]Client
    
    //创建全局channel,传递用户消息
    var message = make(chan string)
    
    func main(){
        //创建监听套接字
        listener,err := net.Listen("tcp","127.0.0.1:8000")
        if err != nil{
            fmt.Println("Listen err",err)
            return
        }
        defer listener.Close()
    
        //创建一个管理者go程,管理map和全局channel
        go Manager()
    
        //循环监听客户端连接请求
        for{
            conn,err := listener.Accept()
            if err != nil{
                fmt.Println("Accept err",err)
                return
            }
            //启动go程处理客户端数据请求
            go HandleConnect(conn)
        }
    }
    
    func Manager() {
        //初始化onlineMap
        onlineMap = make(map[string]Client)
    
        //循环从message中读取
        for{
            //监听全局channel中是否有数据,有数据,存储至msg,无数据阻塞
            msg := <-message
    
            //循环发送消息给所有在线用户
            for _,clnt := range onlineMap{
                clnt.C <- msg
            }
        }
    
    }
    
    func MakeMsg(clnt Client,msg string)(buf string){
        buf = "[" + clnt.Addr + "]" + clnt.Name + ": " + msg
        return
    }
    
    func HandleConnect(conn net.Conn) {
        defer conn.Close()
    
        //创建channel判断用户是否活跃。
        hasData := make(chan bool)
    
        //获取用户网络地址Ip+port
        netAddr := conn.RemoteAddr().String()
    
        //创建新连接用户的结构体,默认用户为IP+Port
        clnt := Client{
            make(chan string),
            netAddr,
            netAddr,
        }
    
        //将新连接用户,添加到在线用户map中
        onlineMap[netAddr] = clnt
    
        //创建专门用来给当前用户发送消息的go程
        go WriteMsgToClient(clnt,conn)
    
        //发送用户上线消息到全局channel中
        //message <- "[" + netAddr + "]" + clnt.Name + "login"
        message <- MakeMsg(clnt,"login")
    
    
        //创建一个channel,用来判断用户退出状态。
        isQuit := make(chan bool)
    
    
        //创建一个匿名go程  专门处理用户发送的消息。
        go func(){
            buf := make([]byte,4096)
            for{
                n,err := conn.Read(buf)
                if n == 0{
                    isQuit <- true
                    fmt.Printf("检测到客户端:%s退出\n",clnt.Name)
                    return
                }
                if err != nil{
                    fmt.Println("conn.Read err:",err)
                    return
                }
                //将读到的消息保存到msg中。
                msg := string(buf[:n - 1])
                //提取在线用户列表
                if msg == "who" && len(msg) == 3{
                    conn.Write([]byte("online user list:\n"))
                    //遍历当前map,获取在线用户
                    for _,user := range onlineMap{
                        userInfo := user.Addr + ":" + user.Name + "\n"
                        conn.Write([]byte(userInfo))
                    }
                }else if len(msg) >= 8 && msg[:6] == "rename"{
                    newName := strings.Split(msg,"|")[1]
                    //msg[8:]
                    clnt.Name = newName //修改结构体成员name
                    onlineMap[netAddr] = clnt   //更新在线用户列表
                    conn.Write([]byte("rename successful\n"))
                }else{
                    //将读到的用户消息广播给所有在线用户,写入到message中
                    message <- MakeMsg(clnt,msg)
                }
                hasData<-true
            }
        }()
    
        //保证不退出
        for{
            //监听channel上的数据流动
            select {
            case <-isQuit:
                delete(onlineMap,clnt.Addr)//将用户从onlineMap中移除
                message <- MakeMsg(clnt,"logout")   //写入用户退出消息到全局channel
                return
            case <- hasData:
                //什么都不做,目的是重置下面case的计时器。
            case <-time.After(time.Second*10):
                delete(onlineMap,clnt.Addr)//将用户从onlineMap中移除
                message <- MakeMsg(clnt,"logout")   //写入用户退出消息到全局channel
                return
            }
        }
    }
    
    func WriteMsgToClient(clnt Client,conn net.Conn){
        //监听用户自带channel上是否有消息。
        for msg := range clnt.C{
            conn.Write([]byte(msg + "\n"))
        }
    }
    
    image.png

    HTTP编程

    image.png
    image.png
    • web工作方式:

        1. 客户端——>访问www.baidu.com ——>DNS服务器。返回该域名对应的IP地址

        2. 客户端——>IP+Port——>访问网页数据(TCP连接。HTTP)

    • HTTP和URL:

      • HTTP超文本传输协议,规定了浏览器访问web服务器进行数据通信的规则。HTTP-TLS、SSL -https(加密)

      • URL:统一资源定位符.在网络环境中唯一定位一个资源数据。(浏览器地址栏内容)。

    • 获取HTTP请求服务器

    func errFunc(err error,info string){
             if err != nil{
             fmt.Println(info,err)
             //return     //返回当前函数调用
             //runtime.Goexit()  //结束当前go程
             os.Exit(1)  //将当前进程结束,0值为正常,所有一般传非0 值。
             }
            }
    func main(){
             listener,err := net.Listen("tcp","127.0.0.1:8000")
             errFunc(err,"net.Listen err:)
             defer listener.Close()
    
             conn,err := listener.Accept()
             errFunc(err,"listener.Accept() err:")
             defer conn.Close()
    
             buf := make([]byte,4096)
             n,err := conn.Read(buf)
             if n == 0{
             return
             }
             errFunc(err,"conn.Read() err:")
           fmt.Printf("[%s]\n",string(buf[:n]))
    }
    
    • http请求包:

      • 请求行:请求方法(空格)请求文件URL(空格)协议版本(\r\n)
        • Get POST
      • 请求头:语法格式:key:value
      • 空行:\r\n --- 代表http请求头结束
      • 请求包体:请求方法对应的数据内容
    • HTTP响应包:

        1. 使用net/http包创建web服务器
          1. 注册回调函数
            1. http.HandleFunc("/itcast",handler)
              1. 参1:用户访问位置
              2. 回调函数名----函数必须是(http.ResponseWriter,*http.Request)作为函数。
          2. 绑定服务器监听地址
            1. http.ListenAndServe("127.0.0.1:8000",nil)
        2. 回调函数:
          1. 本质:函数指针。通过地址,在某一特定位置调用函数
          2. 在程序中,定义一个函数,但 不显示调用,当某一条件满足时,该函数由操作系统自动调用。
    • web服务器:

      • func handler(w http.ResponseWrite,r *http.Request){
            //w:写回给客户端(浏览器)的数据
            //r:从客户端浏览器读到的数据
            w.Write([]byte("hello world"))
        }
        func main(){
            //注册回调函数,该回调函数会在服务器被访问时,自动被调用
            http.HandleFunc("/itcast",handler)
            
            //绑定服务器监听地址
            http.ListenAndServe("127.0.0.1:8000",nil)
        }
        
      • 客户端拿应答包:

        • func main(){
            conn,err := net.Dial("tcp","127.0.0.1:8000")
            if err != nil{
                fmt.Println("net.Dial err:",err)
                return
            }
            defer conn.Close()
            
            httpRequest := "GET /itcast HTTP/1.1\r\nHost:127.0.0.1:8000\r\n\r\n"
            
            conn.Write([]byte(httpRequest))
            
            buf := make([]byte,4096)
            n,_ := conn.Read(buf)
            
            if n==0{
                return
            }
            fmt.Printf("[%s]\n",string(buf[:n]))
          }
          
    image.png
    image.png

    http服务器:

    • func myHandle(w http.ResponseWriter,r *http.Request){
        //w:写给客户端的内容
        w.Write([]byte("this is a Web server"))
        //r:从客户端读到的内容
        fmt.Println("Header:",r.Header)
        fmt.Println("URL:",r.URL)
        fmt.Println("Method:",r.Method)
        fmt.Println("Host:",r.Host)
        fmt.Println("RemoteAddr:",r.RemoteAddr)
        fmt.Println("Body:",r.Body)
      }
      func main(){
        //注册回调函数,在客户端访问时,系统自动调用
        http.HandleFunc("/itcast",myHandle) //如果不指定死"/"或"/itcast/"需要判断访问的URL服务器中是否存在。
        
        //绑定服务器地址
        http.ListenAndServe("127.0.0.1:8000",nil)
      }
      
      1. 注册回调函数 http.HandleFunc("/",myHandler)

        1. func myHandle(w http.ResponseWriter,r *http.Request)

        2. //w:写给客户端的内容

        3. //r:从客户端读到的内容

        4. type ResponseWriter interface{

          ​ Header() Header

          ​ Write([]byte)(int,error)

          ​ WriteHeawder(int)

          }

          type Request struct{

          ​ Method string //浏览器请求方法GET、POST...

          ​ URL *url.URL //浏览器请求的访问路径

          ​ .......

          ​ Header Header //请求头部

          ​ Body io.ReadCloser //请求包体

          ​ RemoteAddr string //浏览器地址

          ​ .......

          ​ ctx context.Context

          }

      2. 绑定服务器地址结构:http.ListenAndServe("127.0.0.1:8000",nil)

        1. 参2:通常传nil,表示让服务器调用默认的http.DefaultServeMux 进行处理
    image.png
    package main
    
    import (
        "fmt"
        "net/http"
        "os"
    )
    
    func OpenSendFile(fName string,w http.ResponseWriter){
        pathFileName := "D:/code/ubuntu_code/Go/src/github.com/cold-rivers-snow/study/GoAdvance/01ptr"+fName
        f,err := os.Open(pathFileName)
        if err != nil{
            fmt.Println("Open err",err)
            w.Write([]byte("No such file or directory!"))
            return
        }
        defer f.Close()
    
        buf := make([]byte,4096)
        for{
            n,_ := f.Read(buf)  //从本地将文件内容读取
            if n==0{
                return
            }
            w.Write([]byte(string(buf[:n])))
        }
    }
    
    func myHandle(w http.ResponseWriter,r *http.Request){
        fmt.Println("URL:",r.URL)
        OpenSendFile(r.URL.String(),w)
    }
    
    func main(){
        //注册回调函数
        http.HandleFunc("/",myHandle)
        //绑定监听地址
        http.ListenAndServe("127.0.0.1:8000",nil)
    }
    
    

    爬虫

    • 概念:访问web服务器,获取指定数据信息的一段程序。

    • 流程:

        1. 构建、发送请求连接

        2. 获取服务器返回的响应数据

        3. 过滤、保存、使用得到的数据。

        4. 关闭请求连接。

    • 聚焦爬虫的工作流程:

        1. 明确URL地址(请求的地址,明确爬什么)

        2. 发送请求,获取响应数据

        3. 保存、过滤响应数据,提取有用信息

        4. 处理数据(存储、使用)

    • 百度贴吧爬虫实现:

        1. 提示用户指定起始、终止页,创建working函数

        2. 使用start,end循环爬取每一页数据

        3. 获取每一页的URL----下一页=前一页+50

        4. 封装、实现HTTPGet函数,爬取一个网页数据的内容,通过result返回。

          1. http.Get/resp.Body.Close/buf := make(4096) for{resp.Body.Read(buf)}/result+=string(buf[:n]) return
        5. 创建.html文件,使用循环因子i命名

        6. 将result写入文件,WriteString(result).f.Close() 不推荐使用defer。

    • https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=50

    • https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=100

    • 下一页+50。关系。

    • 并发版百度贴吧爬虫:

        1. 封装爬取一个网页内容的代码到SpiderPage函数中
        1. 在working函数for循环启动go程 调用SpiderPage()--->n个待爬取页面,对应n个go程

        2. 为防止主go程提前结束,引入channel实现同步,SpiderPage(index,channel)

        3. 在SpiderPage结尾处(一个页面爬取完成),向channel中写内容。channel<-index

        4. 在working函数添加新for循环,从channel不断的读取各个子go程写入的数据,n个子go程--写n次channel -- 读n次channel。

    package main
    
    import (
        "fmt"
        "io"
        "net/http"
        "os"
        "strconv"
    )
    
    func main(){
        //指定爬取起始,终止页
        var start,end int
        fmt.Println("请输入爬取的起始页(>=1):")
        fmt.Scan(&start)
        fmt.Println("请输入爬取的终止页(>=start):")
        fmt.Scan(&end)
    
        working(start,end)
    }
    
    //爬取单个页面的函数
    func SpiderPage(i int,page chan int){
        url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn="+strconv.Itoa((i-1)*50)
        result,err := HttpGet(url)
        if err != nil{
            fmt.Printf("HttpGet err:",err)
            return
        }
        //fmt.Printf("result = ",result)
        //将读到的整网页数据,保存成一个文件
        f,err := os.Create("第 "+strconv.Itoa(i)+" 页"+".html")
        if err != nil{
            fmt.Println("create err:",err)
            return
        }
    
        f.WriteString(result)
        f.Close()   //保存好一个文件,关闭一个文件。
    
        page <- i       //与主go程完成同步
    }
    
    func working(start int, end int) {
        fmt.Printf("正在爬取%d页到%d页.....\n",start,end)
    
        page := make(chan int)
    
        //循环爬取每一页数据
        for i:=start;i <= end;i++{
            go SpiderPage(i,page)
        }
    
        for i:= start;i <= end;i++{
            fmt.Printf("第%d个页面爬取完成\n",<-page)
        }
    }
    
    func HttpGet(url string)(result string,err error) {
        resp,err1 := http.Get(url)
        if err != nil{
            err = err1  //将封装函数内部的错误,传出给调用者
            return
        }
        defer resp.Body.Close()
    
        //循环读取网页数据,传出给调用者
        buf := make([]byte,4096)
        for{
            n,err2 := resp.Body.Read(buf)
            if n==0{
                fmt.Println("读取网页完成")
                break
            }
            if err2 != nil && err2 != io.EOF{
                err = err2
                return
            }
            //累加每一次循环读到的buf数据,存入result一次性返回
            result += string(buf[:n])
        }
        return
    }
    
    

    正则表达式

    在线网站测试正则表达式:https://tool.oschina.net/regex

    能使用string、strings、strconv包函数解决的问题,首选库函数,其次选择正则表达式

    字符类

    -:- -:-
    . 匹配任意一个字符
    [] 匹配[]内任意一个字符
    - 在[]内表示指定范围
    ^ 取反,位于[]开头,匹配除[]之外的任意一个
    [[:xxx:]] 匹配预定义的字符 [[:digit:]]:匹配数字

    次数

    -:- -:-
    匹配前面单元0或1个
    + 匹配前面单元1或多位
    * 匹配前面单元0或多次
    {N} 匹配前面单元精确匹配N次
    {N,} 匹配前面单元至少匹配N次
    {,M} 匹配前面单元至多匹配M次
    {N,M} 匹配前面单元N-M次

    单元限定符

    -:- -:-
    () 将一部分正则表达式组成一个单元,可以对该单元使用数量限定符
    image.png

    Go标准库使用RE2标准

    package main
    
    import (
        "fmt"
        "regexp"
    )
    
    func main(){
        //测试字符串
        str := "abc a7c mfc cat 8ca asc cba"
        //解析,编译正则表达式
        ret := regexp.MustCompile(`a.c`)    //` 表示使用原生字符串
    
        //提取需要信息
        alls := ret.FindAllStringSubmatch(str,-1)   //-1代表全部匹配
        fmt.Println("alls:",alls)
    
    
        //测试小数
        strd := "3.14 123.123 .46.haha 1.0 abc 5. ab.4 r3.3 23."
    
        retd := regexp.MustCompile(`[0-9]\.[0-9]`)
        //retd := regexp.MustCompile(`\d\.\d`)
        allsd := retd.FindAllStringSubmatch(strd,-1)
        fmt.Println("allsd:",allsd)
    }
    

    (?s)单行模式,可以匹配换行符


    image.png
    image.png
    • 步骤:

        1. 解析编译正则表达式

          1. MustCompile(str string) *Regexp

          2. 参数:正则表达式:建议使用反引号--``

          3. 返回值:编译后的正则表达式(结构体类型)

        2. 提取需要的数据:

          1. func (re *Regexp)FindAllStringSubmatch(s string,n int) [][]string

          2. 参1:待匹配的字符串

          3. 参2:匹配次数。-1表示匹配全部

          4. 返回值:存储匹配结构的 [][]string
            数组的每一个成员都有string1 和string2的两个元素。

                - string1:表示,带有匹配参考项的字符串。
                - string2:表示,不包含带有参考项的字符串。
            
    • 提取网页标签数据:

      • 举例:提取<div></div> 之中的数据

          1. ​<div>(.*)</div>

            可以提取div标签中的内容

          2. 对于div标签中含有多行内容:正则表达式(?s:(.*?))

    image.png
    • 双向爬取:
      • 横向:以页为单位
      • 纵向:以一个页中的条目为单位

    爬取豆瓣电影信息:

      1. 获取用户输入起始、终止页,启动toWork函数,循环调用SpiderPage爬取每一个页面
      2. SpiderPage中,获取豆瓣电影横向爬取URL信息,封装HTTPGet函数,爬取一个页面所有数据存入result返回。
      3. 找寻、探索豆瓣网页纵向爬取规律,找出电影名、分数、评分人数的网页数据特征。
      4. 分别对这三部分数据使用go正则函数:1. 解析编译正则表达式2.提取信息->string[1]:代表没有匹配项的内容
      5. 将提取的数据,按照自定义格式写入文件。使用网页编号命名文件。
      6. 实现并发:
        1. go SpiderPage(url)
        2. 创建channel防止主go程退出。
        3. SpiderPage函数末尾,写入channel
        4. 主go程 for 读取channel。
    package main
    
    import (
        "fmt"
        "github.com/aws/aws-sdk-go/service/acm"
        "io"
        "net/http"
        "os"
        "regexp"
        "strconv"
    )
    
    func main(){
        //指定爬取起始页、终止页
        var start,end int
        fmt.Println("请输入爬取的起始页(>=1):")
        fmt.Scan(&start)
        fmt.Println("请输入爬取的终止页(>=start):")
        fmt.Scan(&end)
    
        toWork(start,end)
    }
    
    func toWork(start int, end int) {
        fmt.Printf("正在爬取%d到%d页...\n",start,end)
        page := make(chan int)
    
        for i:= start;i <= end;i++{
            go SpiderPage2(i,page)
        }
    
        for i:= start;i <= end;i++{
            fmt.Println("第%d页爬取完毕\n",<-page)
        }
    }
    
    func SpiderPage2(i int,page chan int) {
        //获取URL地址
        url := "https://movie.douban.com/top250?start="+strconv.Itoa((i-1)*25)+"&filter="
    
        //封装httpget2爬取URL对应页面
        result,err := HttpGet2(url)
        if err != nil{
            fmt.Println("HTTPGet2 err:",err)
            return
        }
        fmt.Println("result = ",result)
    
        //解析编译正则表达式---电影名称
        ret := regexp.MustCompile(`<img width="100" alt="(?s:(.*?))"`)
    
        //提取信息
        fileName := ret.FindAllStringSubmatch(result,-1)
    
        //解析编译正则表达式---分数
        ret2 := regexp.MustCompile(`<span class="rating_num" property="v:average">(.*?)</span>`)
    
        //提取信息
        fileScore := ret2.FindAllStringSubmatch(result,-1)
    
        //解析编译正则表达式---评分人数
        ret3 := regexp.MustCompile(` <span> (.*?) 人评价</span>`)
    
        //提取信息
        peopleNum := ret3.FindAllStringSubmatch(result,-1)
    
        Save2file(i,fileName,fileScore,peopleNum)
    
        page<-i
    }
    
    //爬取指定URL页面,烦result
    func HttpGet2(url string)(result string,err error) {
        resp,err1 := http.Get(url)
        if err1 != nil{
            err = err1
            return
        }
        defer resp.Body.Close()
    
        //ret,err2 := ioutil.ReadAll(resp.Body)
        //if err != nil{
        //  err = err2
        //  return
        //}
        //result = string(ret)
    
        //循环爬取整页数据
        buf := make([]byte,4096)
        for{
            n,err2 := resp.Body.Read(buf)
            if n == 0{
                fmt.Println("无内容")
                break
            }
            if err2 != nil && err2 != io.EOF{
                err = err2
                return
            }
            //result += string(buf[:n])
    
        }
    
        return
    }
    
    func Save2file(i int,fileName,fileScore,peopleNum [][]string){
        path := "D:/code/"+ "第"+strconv.Itoa(i)+"页.txt"
        f,err := os.Create(path)
        if err != nil{
            fmt.Println("os.Create err:",err)
            return
        }
        defer  f.Close()
    
        n := len(fileScore) //得到条目数
    
        //先打印抬头 电影名称   评分   评分人数
        f.WriteString("电影名称"+"\t\t\t"+"评分"+"\t\t"+"评分人数"+"\n")
        for i:=0;i < n;i++{
            f.WriteString(fileName[i][1]+"\t\t\t"+fileScore[i][1]+"\t\t"+peopleNum[i][1]+"\n")
        }
    
    }
    
    • 捧腹网
    package main
    
    import (
        "fmt"
        "github.com/aws/aws-sdk-go/service/codebuild"
        "io"
        "net/http"
        "os"
        "regexp"
        "strconv"
        "strings"
    )
    
    func main(){
        //指定爬取起始页、终止页
        var start,end int
        fmt.Println("请输入爬取的起始页(>=1):")
        fmt.Scan(&start)
        fmt.Println("请输入爬取的终止页(>=start):")
        fmt.Scan(&end)
    
        toWork1(start,end)
    }
    
    func toWork1(start int, end int) {
        fmt.Printf("正在爬取%d到%d页...\n",start,end)
    
        page := make(chan int)
    
        for i:= start;i <= end;i++{
             go SpiderPage3(i,page)
        }
    
        for i:= start;i <= end;i++{
            fmt.Printf("第%d页爬取完毕\n",<-page)
        }
    }
    
    func SpiderPage3(i int,page chan int) {
        url := "https://www.pengfu.com/xiaohua_"+strconv.Itoa(i)+".html"
    
        result,err := HttpGet3(url)
        if err != nil{
            fmt.Println("HttpGet err:",err)
            return
        }
    
        //解析编译正则表达式
        ret := regexp.MustCompile(`<h1 class="dp-b"><a href="(?s:(.*?))"`)
        alls := ret.FindAllStringSubmatch(result,-1)
    
        //创建存储用户titile content的切片
        fileTitle  := make([]string,0)
        fileContent := make([]string,0)
    
        for _,jokeUrl := range alls{
            //fmt.Println("jokeUrl:",jokeUrl[1])
            title,content,err := SpiderJokPage(jokeUrl[1])  //爬取一个页面的title和内容
            if err != nil{
                fmt.Println("SpiderJokPage err:",err)
                return
            }
            //fmt.Println("title:",title)
            //fmt.Println("content:",content)
            fileTitle = append(fileTitle,title)
            fileContent = append(fileContent,content)
        }
        SaveJokefile(i,fileTitle,fileContent)
        page <- i
    }
    
    func SaveJokefile(i int, title []string, content []string) {
        path := "第"+strconv.Itoa(i)+"页.txt"
        f,err := os.Create(path)
        if err != nil{
            fmt.Println("Create err:",err)
            return
        }
        defer f.Close()
        n := len(title)
    
        for i:=0;i<n;i++{
            f.WriteString(title[i]+"\n"+content[i]+"\n")
            f.WriteString("-----------------------------")
        }
    }
    
    func SpiderJokPage(url string)(title,content string,err error){
        result,err1 := HttpGet3(url)
        if err1 != nil{
            err = err1
            return
        }
    
        //编译解析正则表达式---title
        ret1 := regexp.MustCompile(`<h1>(?s:(.*?))</h1>`)
        alls := ret1.FindAllStringSubmatch(result,1)//有2处,取第一个
        for _,tmptitle := range alls{
            title = tmptitle[1]
            title = strings.Replace(title,"\n","",-1)
            title = strings.Replace(title,"\t","",-1)
            break
        }
    
        //编译解析正则表达式---content
        ret2 := regexp.MustCompile(`<div class="content-tx pt10"> (?s:(.*?))<a id="prev" href="`)
        alls2 := ret2.FindAllStringSubmatch(result,1)//有2处,取第一个
        for _,tmpContent := range alls2{
            content = tmpContent[1]
            content = strings.Replace(content,"\n","",-1)
            content = strings.Replace(content,"\t","",-1)
            break
        }
        return
    }
    
    //获取一个网页所有内容
    func HttpGet3(url string)(result string,err error) {
        resp,err1 := http.Get(url)
        if err1 != nil{
            err = err1
            return
        }
        defer resp.Body.Close()
    
        buf := make([]byte,4096)
        for{
            n,err2 := resp.Body.Read(buf)
            if n == 0{
                break
            }
            if err2 != nil && err2 != io.EOF{
                err = err2
                return
            }
            result += string(buf[:n])
        }
        return
    }
    
    package main
    
    import (
        "fmt"
        "io"
        "net/http"
        "os"
        "regexp"
        "strconv"
    )
    
    func main(){
        url := "https://www.douyu.com/g_yz"
    
        result,err := HttpGet4(url)
        if err != nil{
            fmt.Println("HttpGet4 err:",err)
            return
        }
    
    
        //解析正则表达式
        ret := regexp.MustCompile("<img loading=\"lazy\" src=\"(?s:(.*?))\" class=\"DyImg-content is-normal\" alt=\"")
        alls := ret.FindAllStringSubmatch(result,120)
    
        page := make(chan int)
    
        for i,imgUrl := range alls{
            if i == 0{
                continue
            }
            //fmt.Println("imgUrl:",imgUrl[1])
            go SaveImg(imgUrl[1],i,page)
        }
        n := len(alls)
    
        for i:=0;i < n;i++{
            fmt.Printf("下载第%d张图片完成\n",<-page)
        }
    }
    
    func HttpGet4(url string)(result string,err error) {
        resp,err1 := http.Get(url)
        if err1 != nil{
            err = err1
            return
        }
        defer resp.Body.Close()
    
        buf := make([]byte,4096)
        for{
            n,err2 := resp.Body.Read(buf)
            if n == 0{
                break
            }
            if err2 != nil && err2 != io.EOF{
                err = err2
                return
            }
            result += string(buf[:n])
        }
        return
    }
    
    func SaveImg(url string,i int,page chan int){
        path := strconv.Itoa(i)+".png"
        f,err := os.Create(path)
        if err != nil{
            fmt.Println("os.Create err:",err)
            return
        }
        defer f.Close()
    
        resp,err := http.Get(url)
        if err != nil{
            fmt.Println("http.Get err:",err)
            return
        }
        defer resp.Body.Close()
    
        buf := make([]byte,4096)
        for{
            n,err2 := resp.Body.Read(buf)
            if n == 0{
                break
            }
            if err2 != nil && err2 != io.EOF{
                err = err2
                return
            }
            f.Write(buf[:n])
        }
        page<-i
    }
    

    注:最后一张下不下来,就卡死在那里。。。。。

    总结

    • 指针:
      • 指针做函数参数,返回值
    • 切片:
      • buf := make([]byte,4096)
      • 做函数参数,返回值----传引用
      • append函数
    • map:
      • 创建、存储、delete、遍历
    • 结构体:
      • 创建、初始化、传参(默认:传值,推荐:传引用、返回值)
    • 文件:
      • 字符串操作函数:
        • split 、replace、Fields、Itoa、 Atoi、HasSuffix、join
      • 文件处理函数:
        • Open、Create 、OpenFile、Readdir
      • 文件操作练习,大文件拷贝。
    • 并发:
      • 进程、程序、线程、协程、go程。
      • 进程状态
      • 同步 --- 基本同步方法。
      • goroutine特性。
      • Goexit()、return 、os.Exit(2)区别和联系
    • channel:
      • 创建
      • 无缓冲、有缓冲channel
      • 关闭channel特性。 --- 读写都在,不读、不写 -- 阻塞。
      • 单向channel ---- 传参
      • 生产者、消费者
      • 定时器:Timer、time.After、
      • select:监听channel上的数据流动。
      • 死锁:单go程死锁、两个go程先后顺序死锁、交叉死锁。
      • 同步方法:互斥量、读写锁、条件变量---生产者、消费者(并发)
    • 网络编程:
      • 协议:OSI7层、TCP/IP4层,各层代表协议
      • 数据通信过程:数据---应用层---传输层---网络层----链路层---网络环境。
      • socket,BS、CS设计模式
      • TCP-CS通信。--- 并发版
      • TCP通信过程:三次握手、四次挥手--画图讲解
      • UDP通信:CS通信代码实现---并发版
      • UDP与TCP区别:优缺点---使用场景。
      • 网络文件传输。大文件拷贝+TCP-CS。
      • 并发聊天室:----以前知识的集合(cs通信、channel、goroutine、map、切片、结构体、select)--实现。
    • HTTP编程:
      • web服务器工作方式:DNS服务器,HTTP,URL
      • HTTP请求协议基本格式。
      • HTTP应答协议基本格式。
      • GO函数,实现HTTP web服务器,模拟客户端。
    • 爬虫开发:
      • 爬虫工作流程:4步
      • 横向、纵向爬虫
      • 正则表达式
      • 百度贴吧、段子(捧腹网)、豆瓣电影、斗鱼。

    相关文章

      网友评论

          本文标题:GoAdvance

          本文链接:https://www.haomeiwen.com/subject/hrrwahtx.html