Go语言基础 - 基础语法 | 青训营笔记

112 阅读8分钟

这是我参与「第五届青训营」伴学笔记的第1天

阅前提示: 由于该篇笔记为后期整理,所以里面有一些第一节课讲到了的基础语法并不一定会在该笔记中,但是可能会出现在后续笔记~

一、go语言基础语法

1.1 变量与数据类型

1.1.1 变量

 //变量的四种声明方式:
 //1.指定变量类型并赋值
 var a int = 2
 //2.指定变量类型但不赋值
 var b int
 //3.不指定变量类型,根据‘=’后面的值判定变量的类型(自动类型推断,和python有点相似)
 var c = 3
 //4.省略var,使用:=(这种赋值方式和Pascal语言有点相似)
 d := 4

注:go语言还支持一次性声明多个变量(多变量声明)

 var n1,n2,n3 int
 var n4,name,n5 = 10,"jack",7.8
 n6,height := 6.9,100.6

1.1.2 常量

常量和变量一样,将var改成const即可

1.1.3 数据类型

image.png

1.2 流程控制

1.2.1 分支

1.2.1.1 if else

基本语法:

 if 条件表达式1 { 
      ... 
 } else if 条件表达式2 { 
      ... 
 }else { ... }

注: 1.Golang里面,{}是必须有的,只有一行代码也是如此 2.条件表达式外的()可以不写

1.2.1.2 switch

基本语法:

 switch 表达式 {
   case1,值2,.….:
        语句块1
   case3,值4,...:
        语句块2
         ....
   default:
        语句块3
 }

注: 1.switch后是一个表达式(常量值、变量、一个有返回值的函数等都可以) 2.表达式要求: 常量值:不能重复 变量:数据类型必须要switch的表达式数据类型一致 多个值:使用逗号间隔 3.case后面不需要break 4.switch穿透:添加fallthrough

1.2.2 循环

基本语法:

 for 初始表达式; 布尔表达式; 迭代因子 {
           循环体
 }

键值循环:

for range结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道,for range 语法上类似于其它语言中的 foreach 语句,一般形式为:

 for key, val := range arr {
     ...
 }

其中,key是arr中的下标,val是arr中下标对应的值

1.3 函数

基本语法:

 func   函数名(形参列表)(返回值类型列表){
      执行语句..
      return + 返回值列表
 }

例子:

 //自定义函数:功能:两个数相加:
 func cal (num1 int,num2 int) (int) { //如果返回值类型就一个的话,那么()是可以省略不写的
         var sum int = 0
         sum += num1
         sum += num2
         return sum
 }
 func main(){
         //功能:10 + 20
         //调用函数:
         sum := cal(10,20)
 }

注: 1.首字母大写该函数可以被本包文件和其它包文件使用(类似public) 2.首学母小写只能被本包文件使用,其它包文件不能使用(类似private) 3.如果有返回值不想接受,可以使用_忽略 4.函数不支持重载 5.支持可变参数,如下:

 package main
 import "fmt"
 //定义一个函数,函数的参数为:可变参数 ...  参数的数量可变
 //args...int 可以传入任意多个数量的int类型的数据  传入0个,1个,,,,n个
 func test (args...int){
         //函数内部处理可变参数的时候,将可变参数当做切片来处理
         //遍历可变参数:
         for i := 0; i < len(args); i++ {
                 fmt.Println(args[i])
         }
 }
 func main(){   
         test()
         test(3)
         test(37,58,39,59,47)
 }

6.传入变量的地址&,该变量的值如果在函数内修改,函数外也会修改,否则该变量的值仅在函数内修改,而函数外不会改变。如下:

 package main
 import "fmt"
 //自定义函数:功能:交换两个数
 func exchangeNum (num1 int,num2 int){ 
         var t int
         t = num1
         num1 = num2
         num2 = t
 }
 ​
 func realExchangeNum (num1 *int,num2 *int){ 
         var t int
         t = *num1
         *num1 = *num2
         *num2 = t
 }
 ​
 func main(){   
         //调用函数:交换10和20
         var num1 int = 10
         var num2 int = 20
         fmt.Printf("交换前的两个数: num1 = %v,num2 = %v \n",num1,num2)//10 20
         exchangeNum(num1,num2)
         fmt.Printf("交换1后的两个数: num1 = %v,num2 = %v \n",num1,num2)//10 20
         realExchangeNum(&num1,&num2)
         fmt.Printf("交换2后的两个数: num1 = %v,num2 = %v \n",num1,num2)//20 10
 }

7.函数也是一种数据类型,可以赋值给一个变量,通过该变量可以对函数调用 8.Golang支持函数作为形参被调用

 package main
 import "fmt"
 //定义一个函数:
 func test(num int){
         fmt.Println(num)
 }
 //定义一个函数,把另一个函数作为形参:
 func test02 (num1 int ,num2 float32, testFunc func(int)){
         fmt.Println("-----test02")
 }
 func main(){
         //函数也是一种数据类型,可以赋值给一个变量     
         a := test//变量就是一个函数类型的变量
         fmt.Printf("a的类型是:%T,test函数的类型是:%T \n",a,test)//a的类型是:func(int),test函数的类型是:func(int)
         //通过该变量可以对函数调用
         a(10) //等价于  test(10)
         //调用test02函数:
         test02(10,3.19,test)
         test02(10,3.19,a)
 }

1.4 数组和切片

1.4.1 数组

注: 1.go里面数组是指指定长度大小的数组。不像C++里面,不指定长度大小也是数组。 2.数组在go里面,大小不一样,它的类型是不一样的。 3.数组在函数传参时采用的是传值拷贝。因此函数内部无法改变外部的数组大小。

基本语法:

 var 数组名 [数组大小]数据类型

声明方式:

 package main
 import "fmt"
 func main(){
         //第一种:
         var arr1 [3]int = [3]int{3,6,9}
         fmt.Println(arr1)
         //第二种:
         var arr2 = [3]int{1,4,7}
         fmt.Println(arr2)
         //第三种:
         var arr3 = [...]int{4,5,6,7}
         fmt.Println(arr3)
      //第四种:  ':'前面的数字代表的是它们在数组中的下标
         var arr4 = [...]int{2:66,0:33,1:99,3:88}
         fmt.Println(arr4)
 }

1.4.2 切片

注: 1.slice是切片,定义类似数组,但是定义的时候不带上固定大小 2.切片一定不加大小! 3.切片在函数传参时采用的是引用。因此函数内部可以改变外部的切片大小。

基本语法:

 var 数组名 []数据类型

声明方式:

 //第一种声明方式
 slice1 := []int{1, 2, 3}
 fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1)
 ​
 //第二种声明方式,没有分配空间
 var slice2 []int
 slice2 = make([]int, 3) //make可以为slice2分配空间,默认值为0
 fmt.Printf("len = %d, slice = %v\n", len(slice2), slice2)
 ​
 //第三种声明方式,直接用make
 var slice3 = make([]int, 3)
 fmt.Printf("len= %d, slice = %v\n", len(slice3), slice3)
 ​
 //第四种:= + make,一般用这一种
 slice4 := make([]int, 3)
 fmt.Printf("len= %d, slice = %v\n", len(slice4), slice4)

1.4.2.1 追加和截取

1.4.2.1.1 追加

追加这里类似vector

      a := make([]int, 1)
      a[0] = 2
      a = append(a, 3)
      fmt.Println(a)// [2 3]
1.4.2.1.2 截取
      a := make([]int, 1)
      a[0] = 2
      a = append(a, 3)
      fmt.Println(a)// [2 3]
      //左闭右开
      t := a[0:1]
      t[0] = 10
      fmt.Println(a)// [10 3]

注:t和a是同一个切片。t只是a的引用而已。它们两个底层的数组是一样的。如果只是想拷贝slice里面的元素到新的slice,使用copy函数(copy(t,a),把a的值拷贝给t)

1.5 映射(map)

基本语法:

 var 变量名 map[key类型]value类型

注: 1.key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的 接口、结构体、数组 2.key:通常为int 、string类型;slice、map、function不可以 3.value:通常为数字(整数、浮点数)、string、map、结构体 4.map在函数传参时采用的是引用。因此函数内部可以改变外部的map。

声明方式:

 第一种声明,这个map是空的
 var mymap1 map[string]string
 if mymap1 == nil {
      fmt.Println("mymap1 is empty")
 }
 ​
 第二种声明,声明+make开辟空间
 var mymap2 = make(map[string]string, 10)
 mymap2["c++"] = "1"
 mymap2["java"] = "2"
 ​
 第三种:= + make
 mymap3 := make(map[string]string) //make会自己给map一点空间的
 mymap3["c++"] = "1"
 mymap3["java"] = "2"
 ​
 第四种:= + 大括号初始化定义
 mymap4 := map[string]string{"c++": "one", "java": "two"}
 fmt.Println(mymap4)`
 ​

1.5.1 删除

删除关键字,使用delete

 delete(mymap4, "c++")

二、实战

2.1 猜谜游戏

2.1.1 原代码

  • 生成随机数作为被猜的数字

    代码如下:

     package main
     import (
          "fmt"
          "math/rand"
     )
     func main() {
          maxNum := 100
          secretNumber := rand.Intn(maxNum)
          fmt.Println("The secret number is :", secretNumber)
     }
    

    Q1.每一次运行程序,都只会生成同一个数字

    原因:未设置初始的随机数种子,此时程序里面的随机数种子序列每一次都是固定的,所以会造成每一次重新启动程序,都只会生成同一个数字

    解决方案:用时间戳初始化随机数种子

          rand.Seed(time.Now().UnixNano())
    
  • 读取用户输入

     package main
     ​
     import (
        "bufio"
        "fmt"
        "math/rand"
        "os"
        "strconv"
        "strings"
        "time"
     )
     func main() {
        //设置被猜的随机数
        maxNum := 100
        rand.Seed(time.Now().UnixNano())
        secretNumber := rand.Intn(maxNum)
        fmt.Println("The secret number is :", secretNumber)
     ​
        fmt.Println("Please input your guess")
        reader := bufio.NewReader(os.Stdin)
        //读取一行输入
        input, err := reader.ReadString('\n')
        if err != nil {
           fmt.Println("An error occured while reading input. Please try again", err)
           return
           }
         //去掉换行符
         input = strings.Trim(input, "\r\n")
         //将字符串转换成数字
         guess, err := strconv.Atoi(input)
         if err != nil {
            fmt.Println("Invalid input. Please enter an integer value")
            return
         }
         fmt.Println("You guess is", guess)
     }
    
  • 实现游戏循环

     package main
     ​
     import (
          "bufio"
          "fmt"
          "math/rand"
          "os"
          "strconv"
          "strings"
          "time"
     )
     ​
     func main() {
          maxNum := 100
          rand.Seed(time.Now().UnixNano())
          secretNumber := rand.Intn(maxNum)
          // fmt.Println("The secret number is ", secretNumber)
     ​
          fmt.Println("Please input your guess")
          reader := bufio.NewReader(os.Stdin)
          for {
               input, err := reader.ReadString('\n')
               if err != nil {
                    fmt.Println("An error occured while reading input. Please try again", err)
                    continue
               }
               input = strings.Trim(input, "\r\n")
     ​
               guess, err := strconv.Atoi(input)
               if err != nil {
                    fmt.Println("Invalid input. Please enter an integer value")
                    continue
               }
               fmt.Println("You guess is", guess)
               if guess > secretNumber {
                    fmt.Println("Your guess is bigger than the secret number. Please try again")
               } else if guess < secretNumber {
                    fmt.Println("Your guess is smaller than the secret number. Please try again")
               } else {
                    fmt.Println("Correct, you Legend!")
                    break
               }
          }
     }
    
  • 最终效果:

image.png

2.2 在线词典

2.2.1 原代码

 package main
 ​
 import (
      "bytes"
      "encoding/json"
      "fmt"
      "io/ioutil"
      "log"
      "net/http"
 )
 ​
 type DictRequest struct {
      TransType string `json:"trans_type"`
      Source    string `json:"source"`
      UserID    string `json:"user_id"`
 }
 ​
 type DictResponse struct {
      Rc   int `json:"rc"`
      Wiki struct {
           KnownInLaguages int `json:"known_in_laguages"`
           Description     struct {
                Source string      `json:"source"`
                Target interface{} `json:"target"`
           } `json:"description"`
           ID   string `json:"id"`
           Item struct {
                Source string `json:"source"`
                Target string `json:"target"`
           } `json:"item"`
           ImageURL  string `json:"image_url"`
           IsSubject string `json:"is_subject"`
           Sitelink  string `json:"sitelink"`
      } `json:"wiki"`
      Dictionary struct {
           Prons struct {
                EnUs string `json:"en-us"`
                En   string `json:"en"`
           } `json:"prons"`
           Explanations []string      `json:"explanations"`
           Synonym      []string      `json:"synonym"`
           Antonym      []interface{} `json:"antonym"`
           WqxExample   [][]string    `json:"wqx_example"`
           Entry        string        `json:"entry"`
           Type         string        `json:"type"`
           Related      []interface{} `json:"related"`
           Source       string        `json:"source"`
      } `json:"dictionary"`
 }
 ​
 func query(word string) {
      client := &http.Client{}
      request := DictRequest{TransType: "en2zh", Source: word, UserID: "63cd42721120a60014fbd804"}
      //结构体转为json
      buf, err := json.Marshal(request)
      if err != nil {
           log.Fatal(err)
      }
      var data = bytes.NewReader(buf)
      //设置请求
      req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
      if err != nil {
           log.Fatal(err)
      }
      //设置请求头
      req.Header.Set("authority", "api.interpreter.caiyunai.com")
      req.Header.Set("pragma", "no-cache")
      req.Header.Set("cache-control", "no-cache")
      req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="8"`)
      req.Header.Set("sec-ch-ua-mobile", "?0")
      req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 SLBrowser/8.0.0.12022 SLBChan/25")
      req.Header.Set("app-name", "xy")
      req.Header.Set("content-type", "application/json;charset=UTF-8")
      req.Header.Set("accept", "application/json, text/plain, */*")
      req.Header.Set("os-type", "web")
      req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
      req.Header.Set("origin", "https://fanyi.caiyunapp.com")
      req.Header.Set("sec-fetch-site", "cross-site")
      req.Header.Set("sec-fetch-mode", "cors")
      req.Header.Set("sec-fetch-dest", "empty")
      req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
      req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
      //发送请求
      resp, err := client.Do(req)
      if err != nil {
           log.Fatal(err)
      }
      defer resp.Body.Close()
      bodyText, err := ioutil.ReadAll(resp.Body)
      if err != nil {
           log.Fatal(err)
      }
      if resp.StatusCode != 200 {
           log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
      }
      var dictResponse DictResponse
      //json转化成结构体
      err = json.Unmarshal(bodyText, &dictResponse)
      if err != nil {
           log.Fatal(err)
      }
      //打印我们需要的字符部分
      fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
      for _, item := range dictResponse.Dictionary.Explanations {
           fmt.Println(item)
      }
 }
 ​
 func main() {
      var word string
      fmt.Scanf("%s", &word)
      query(word)
 }

有关在线词典的实现实际上是数据的爬取和清洗

前面设置请求头的地方实则是将程序伪装成一个浏览器,然后再向服务器发送请求(不然服务器会发现你是个爬虫,从而获取不到数据)

而后面将获取到的json数据转为结构体,则是为下面清洗数据做准备,方便我们找到需要的数据所在的位置

2.3 socks5代理

  • TCP echo server

实现一个简版的TCP echo server,用来测试我们的server是否正确,该程序仅能做到我们发送什么,它就回复什么

 package main
 ​
 import (
      "bufio"
      "log"
      "net"
 )
 ​
 func main() {
      server, err := net.Listen("tcp", "127.0.0.1:1080")
      if err != nil {
           panic(err)
      }
      for {
           client, err := server.Accept()
           if err != nil {
                log.Println("Accept failed %v", err)
                continue
           }
           go process(client)
      }
 }
 ​
 func process(conn net.Conn) {
      defer conn.Close()
      reader := bufio.NewReader(conn)
      for {
           b, err := reader.ReadByte()
           if err != nil {
                break
           }
           _, err = conn.Write([]byte{b})
           if err != nil {
                break
           }
      }
 }
 ​

注:在运行完该代码以后需要使用nc命令,该命令需要下载

后面的步骤基于简版的TCP echo server框架进行

  • auth

    基于TCP的客户端连接过程

    • 客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方式:

                          +----+----------+----------+
                          |VER | NMETHODS | METHODS  |
                          +----+----------+----------+
                          | 1  |    1     | 1 to 255 |
                          +----+----------+----------+
      

      如果是socks5代理,第一个字段VER的值是0x05,表明是socks代理的第5个版本。

      第二个字段NMETHODS表示支持的认证方式,第三个字段是一个数组,包含了支持的认证方式列表:

      • 0x00: 不需要认证
      • 0x01: GSSAPI认证
      • 0x02: 用户名和密码方式认证
      • 0x03: IANA认证
      • 0x80-0xfe: 保留的认证方式
      • 0xff: 不支持任何认证方式
    • 服务端收到客户端的代理请求后,选择双方都支持的加密方式回复给客户端:

                              +----+--------+
                              |VER | METHOD |
                              +----+--------+
                              | 1  |   1    |
                              +----+--------+
    

    此时客户端收到服务端的响应请求后,双方握手完成,开始进行协议交互。

    上述过程代码如下:

     func auth(reader *bufio.Reader, conn net.Conn) (err error) {
          // +----+----------+----------+
          // |VER | NMETHODS | METHODS  |
          // +----+----------+----------+
          // | 1  |    1     | 1 to 255 |
          // +----+----------+----------+
          // VER: 协议版本,socks5为0x05
          // NMETHODS: 支持认证的方法数量
          // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
          // X’00’ NO AUTHENTICATION REQUIRED
          // X’02’ USERNAME/PASSWORD
     ​
          ver, err := reader.ReadByte()
          if err != nil {
               return fmt.Errorf("read ver failed:%w", err)
          }
          if ver != socks5Ver {
               return fmt.Errorf("not supported ver:%v", ver)
          }
          methodSize, err := reader.ReadByte()
          if err != nil {
               return fmt.Errorf("read methodSize failed:%w", err)
          }
          method := make([]byte, methodSize)
          _, err = io.ReadFull(reader, method)
          if err != nil {
               return fmt.Errorf("read method failed:%w", err)
          }
     ​
          // +----+--------+
          // |VER | METHOD |
          // +----+--------+
          // | 1  |   1    |
          // +----+--------+
          _, err = conn.Write([]byte{socks5Ver, 0x00})
          if err != nil {
               return fmt.Errorf("write failed:%w", err)
          }
          return nil
     }
    

    与此同时,还需要修改process函数

     func process(conn net.Conn) {
          defer conn.Close()
          reader := bufio.NewReader(conn)
          err := auth(reader, conn)
          if err != nil {
               log.Println("client %v auth failed:%v", conn.RemoteAddr(), err)
               return
          }
          log.Println("auto success")
     }
    
  • 请求阶段

    1. 请求:

      握手完成后,客户端要把需要执行的操作指令发给服务端,表明自己要执行代理的请求。请求帧格式:

               +----+-----+-------+------+----------+----------+
               |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
               +----+-----+-------+------+----------+----------+
               | 1  |  1  | X'00' |  1   | Variable |    2     |
               +----+-----+-------+------+----------+----------+
      

      各字段含义:

      • VER: 代理版本信息

      • CMD: 代理指令

        • 0x01: connect指令,tcp代理时使用。
        • 0x02: bind,很少使用,类似FTP协议中主动连接场景,服务端后服务端会主动连接到客户端。
        • 0x03: udp代理时使用。
      • RSV: 保留字段

      • ATYP: 地址类型

        • 0x01: IPv4地址类型
        • 0x03: unix域socket类型代理
        • 0x04: IPv6地址类型
      • DST.ADDR: 需要连接的目的地址

      • DST.PORT: 需要连接的目的端口

    2. 响应:

      客户端发完上面的请求连接后,服务端会发起连接到DST.ADDR:DST.PORT,然后返回响应到客户端,响应格式:

               +----+-----+-------+------+----------+----------+
               |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
               +----+-----+-------+------+----------+----------+
               | 1  |  1  | X'00' |  1   | Variable |    2     |
               +----+-----+-------+------+----------+----------+
       12345
      

      其中VER/RSV/ATYP的含义和上面相同,其他字段的意思:

      • REP: 请求响应

        • 0x00: 成功
        • 0x01-0x08: 失败
        • 0x09-0xff: 未使用
      • BND.ADDR: 连接到的远程地址

      • BND.PORT: 连接到的远程端口

    上述过程代码如下:

     func connect(reader *bufio.Reader, conn net.Conn) (err error) {
          // +----+-----+-------+------+----------+----------+
          // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
          // +----+-----+-------+------+----------+----------+
          // | 1  |  1  | X'00' |  1   | Variable |    2     |
          // +----+-----+-------+------+----------+----------+
          // VER 版本号,socks5的值为0x05
          // CMD 0x01表示CONNECT请求
          // RSV 保留字段,值为0x00
          // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
          //   0x01表示IPv4地址,DST.ADDR为4个字节
          //   0x03表示域名,DST.ADDR是一个可变长度的域名
          // DST.ADDR 一个可变长度的值
          // DST.PORT 目标端口,固定2个字节
     ​
          buf := make([]byte, 4)
          _, err = io.ReadFull(reader, buf)
          if err != nil {
               return fmt.Errorf("read header failed:%w", err)
          }
          ver, cmd, atyp := buf[0], buf[1], buf[3]
          if ver != socks5Ver {
               return fmt.Errorf("not supported ver:%v", ver)
          }
          if cmd != cmdBind {
               return fmt.Errorf("not supported cmd:%v", ver)
          }
          addr := ""
          switch atyp {
          case atypIPV4:
               _, err = io.ReadFull(reader, buf)
               if err != nil {
                    return fmt.Errorf("read atyp failed:%w", err)
               }
               addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
          case atypeHOST:
               hostSize, err := reader.ReadByte()
               if err != nil {
                    return fmt.Errorf("read hostSize failed:%w", err)
               }
               host := make([]byte, hostSize)
               _, err = io.ReadFull(reader, host)
               if err != nil {
                    return fmt.Errorf("read host failed:%w", err)
               }
               addr = string(host)
          case atypeIPV6:
               return errors.New("IPv6: no supported yet")
          default:
               return errors.New("invalid atyp")
          }
          _, err = io.ReadFull(reader, buf[:2])
          if err != nil {
               return fmt.Errorf("read port failed:%w", err)
          }
          port := binary.BigEndian.Uint16(buf[:2])
     ​
          log.Println("dial", addr, port)
     ​
          // +----+-----+-------+------+----------+----------+
          // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
          // +----+-----+-------+------+----------+----------+
          // | 1  |  1  | X'00' |  1   | Variable |    2     |
          // +----+-----+-------+------+----------+----------+
          // VER socks版本,这里为0x05
          // REP Relay field,内容取值如下 X’00’ succeeded
          // RSV 保留字段
          // ATYPE 地址类型
          // BND.ADDR 服务绑定的地址
          // BND.PORT 服务绑定的端口DST.PORT
          _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
          if err != nil {
               return fmt.Errorf("write failed: %w", err)
          }
          
          return nil
     }
    

    与此同时,修改process函数

     func process(conn net.Conn) {
          defer conn.Close()
          reader := bufio.NewReader(conn)
          err := auth(reader, conn)
          if err != nil {
               log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
               return
          }
          err = connect(reader, conn)
          if err != nil {
               log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
               return
          }
     }
    
  • relay阶段

    在请求阶段中的connect函数中第57行添加如下代码:

     dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
          if err != nil {
               return fmt.Errorf("dial dst failed:%w", err)
          }
          defer dest.Close()
    

    第75行添加如下代码:

     ctx, cancel := context.WithCancel(context.Background())
          defer cancel()
     ​
          go func() {
               _, _ = io.Copy(dest, reader)
               cancel()
          }()
          go func() {
               _, _ = io.Copy(conn, dest)
               cancel()
          }()
     ​
          <-ctx.Done()
    

三、作业

3.1 修改第一个例子猜谜游戏里面的最终代码,使用fmt.Scanf来简化代码实现

 package main
 ​
 import (
      "fmt"
      "math/rand"
      "time"
 )
 ​
 func main() {
      maxNum := 100
      rand.Seed(time.Now().UnixNano())
      secretNumber := rand.Intn(maxNum)
      // fmt.Println("The secret number is ", secretNumber)
 ​
      fmt.Println("Please input your guess")
      var guess int
      for {
           fmt.Scanf("%d ", &guess)
           fmt.Println("You guess is", guess)
           if guess > secretNumber {
                fmt.Println("Your guess is bigger than the secret number. Please try again")
           } else if guess < secretNumber {
                fmt.Println("Your guess is smaller than the secret number. Please try again")
           } else {
                fmt.Println("Correct, you Legend!")
                break
           }
      }
 }

Q1. 第二次走到fmt.Scanf时,程序跳过了输入

原因:

img

所以直接使用fmt.Scanf("%d", &guess)会返回错误信息,而这也导致了第二次走到fmt.Scanf时,程序会跳过输入

解决:

 fmt.Scanf("%d ", &guess)
 //或者
 fmt.Scanf("%d\n", &guess)

3.2 修改第二个例子命令行词典里面的最终代码,增加另一种翻译引擎的支持

 package main
 ​
 import (
      "bytes"
      "encoding/json"
      "fmt"
      "io/ioutil"
      "log"
      "net/http"
 )
 ​
 //彩云小译
 type DictRequest struct {
      TransType string `json:"trans_type"`
      Source    string `json:"source"`
      UserID    string `json:"user_id"`
 }
 ​
 type DictResponse struct {
      Rc   int `json:"rc"`
      Wiki struct {
           KnownInLaguages int `json:"known_in_laguages"`
           Description     struct {
                Source string      `json:"source"`
                Target interface{} `json:"target"`
           } `json:"description"`
           ID   string `json:"id"`
           Item struct {
                Source string `json:"source"`
                Target string `json:"target"`
           } `json:"item"`
           ImageURL  string `json:"image_url"`
           IsSubject string `json:"is_subject"`
           Sitelink  string `json:"sitelink"`
      } `json:"wiki"`
      Dictionary struct {
           Prons struct {
                EnUs string `json:"en-us"`
                En   string `json:"en"`
           } `json:"prons"`
           Explanations []string      `json:"explanations"`
           Synonym      []string      `json:"synonym"`
           Antonym      []interface{} `json:"antonym"`
           WqxExample   [][]string    `json:"wqx_example"`
           Entry        string        `json:"entry"`
           Type         string        `json:"type"`
           Related      []interface{} `json:"related"`
           Source       string        `json:"source"`
      } `json:"dictionary"`
 }
 ​
 //沪江小D
 type DDictRes struct {
      Data struct {
           Content       string `json:"content"`
           FromLang      string `json:"fromLang"`
           OperationDate string `json:"operationDate"`
           OriginalText  string `json:"original_text"`
           Pronounce     struct {
                Audio []struct {
                     Length    int    `json:"length"`
                     OriginURL string `json:"originUrl"`
                     Start     int    `json:"start"`
                     URL       string `json:"url"`
                } `json:"audio"`
                AudioOriginStatus int `json:"audioOriginStatus"`
                AudioStatus       int `json:"audioStatus"`
           } `json:"pronounce"`
           Source string `json:"source"`
           ToLang string `json:"toLang"`
      } `json:"data"`
      Message string `json:"message"`
      Status  int    `json:"status"`
      Time    string `json:"time"`
 }
 ​
 //设置请求头
 func getRequestHead(interType string, req *http.Request) {
      switch interType {
      case "Color":
           req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 SLBrowser/8.0.0.12022 SLBChan/25")
           req.Header.Set("content-type", "application/json;charset=UTF-8")
           req.Header.Set("accept", "application/json, text/plain, */*")
           req.Header.Set("os-type", "web")
           req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
           req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
      case "D":
           req.Header.Set("accept", "*/*")
           req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
           req.Header.Set("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
           req.Header.Set("cookie", "HJ_UID=635ed610-2a74-89b3-6c2b-4ea12664ef04; HJ_CST=1; HJ_CSST_3=1; TRACKSITEMAP=3; _REF=https%3A%2F%2Fwww.google.com.hk%2F; _SREF_3=https%3A%2F%2Fwww.google.com.hk%2F; HJ_SID=i2xho7-efa7-44e1-b5db-18f0944def46; HJ_SSID_3=i2xho7-3b27-4710-995c-edc285bcd33d; _SREG_3=www.google.com.hk%7C%7Csearch%7Cdomain; _REG=www.google.com.hk%7C%7Csearch%7Cdomain")
           req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
           req.Header.Set("x-requested-with", "XMLHttpRequest")
      }
 }
 ​
 //打印翻译结果
 func print(interType string, bodyText []byte, word string) {
      switch interType {
      case "Color":
           var dictResponse DictResponse
           //json转化成结构体
           err := json.Unmarshal(bodyText, &dictResponse)
           if err != nil {
                log.Fatal(err)
           }
           //打印我们需要的字符部分
           fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
           for _, item := range dictResponse.Dictionary.Explanations {
                fmt.Println(item)
           }
      case "D":
           var dDictRes DDictRes
           //json转化成结构体
           err := json.Unmarshal(bodyText, &dDictRes)
           if err != nil {
                log.Fatal(err)
           }
           fmt.Println(word, "\n", dDictRes.Data.Content)
      }
 }
 ​
 func query(word string, interpretType string) {
      client := &http.Client{}
 ​
      var data *bytes.Reader
      var url string
      if interpretType == "Color" {
           request := DictRequest{TransType: "en2zh", Source: word, UserID: "63cd42721120a60014fbd804"}
           //结构体转为json
           buf, err := json.Marshal(request)
           if err != nil {
                log.Fatal(err)
           }
           data = bytes.NewReader(buf)
           url = "https://api.interpreter.caiyunai.com/v1/dict"
      } else {
           data = bytes.NewReader([]byte(`content=` + word))
           url = "https://dict.hujiang.com/v10/dict/translation/en/cn"
      }
 ​
      //设置请求
      req, err := http.NewRequest("POST", url, data)
      if err != nil {
           log.Fatal(err)
      }
 ​
      //设置请求头
      getRequestHead(interpretType, req)
 ​
      //发送请求
      resp, err := client.Do(req)
      if err != nil {
           log.Fatal(err)
      }
      defer resp.Body.Close()
      bodyText, err := ioutil.ReadAll(resp.Body)
      if err != nil {
           log.Fatal(err)
      }
      if resp.StatusCode != 200 {
           log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
      }
 ​
      //打印翻译结果
      print(interpretType, bodyText, word)
 }
 ​
 func main() {
      var word string
      var interType string
      fmt.Println("please input the interpretType and the word need to interpret:")
      fmt.Scanf("%s %s", &interType, &word)
      query(word, interType)
 }

3.3 在上一步骤的基础上,修改代码实现并行请求两个翻译引擎来提高响应速度

仅需在3.2的代码基础上,修改main函数即可,代码如下:

image.png Q1.如上我们可以发现,它并没有起到并发的作用,甚至都没有进行翻译器的功能

原因:主线程main比协程执行得快,主线程main执行完毕退出以后,协程即使还没有执行完毕也会退出,所以翻译器这部分的功能所在的协程未执行完毕即退出,导致翻译功能未实现

解决方法:使用WaitGroup控制协程退出,代码如下:

 func main() {
      var word string
      fmt.Println("please input the interpretType and the word need to interpret:")
      fmt.Scanf("%s", &word)
      for i := 0; i < 2; i++ {
           wg.Add(1)
           go func(n int) {
                if n == 0 {
                     query(word, "D")
                } else {
                     query(word, "Color")
                }
                wg.Done()
           }(i)
      }
      wg.Wait()
      fmt.Println("the main has finished!\n")
 }