GoAdvance

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:表示,不包含带有参考项的字符串。
  • 提取网页标签数据:

    • 举例:提取

      之中的数据

        1. (.*)

          可以提取div标签中的内容

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

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

  • https://movie.douban.com/top250?start=0&filter= 1

  • https://movie.douban.com/top250?start=25&filter= 2

  • https://movie.douban.com/top250?start=50&filter= 3

  • https://movie.douban.com/top250?start=75&filter= 4

  • 纵向:

    • 电影名称: 电影名称“ ---> <code><img width=

    • 分数:分数 ----> (?s:(.*?))

    • 评分人数: 评分人数 人评价 ---> (?s:(.*?)) 人评价

爬取豆瓣电影信息:

    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(`(?s:(.*?))(.*?)`)

    //提取信息
    fileScore := ret2.FindAllStringSubmatch(result,-1)

    //解析编译正则表达式---评分人数
    ret3 := regexp.MustCompile(`  (.*?) 人评价`)

    //提取信息
    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")
    }

}
  • 捧腹网