ByteDay1《Go语言入门上手-基础语言》 | 青训营笔记

104 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第 1 篇笔记

Go 语言上手 - 基础语言

一、本堂课重点内容:

  • 切片
  • 指针
  • 结构体方法
  • JSON 处理
  • 随机数处理
  • 标准 io 流
  • Socks5代理原理

二、详细知识点介绍:

本堂课介绍了哪些知识点?

切片

切片不同于数组可以任意更改长度,有许多丰富的操作,可以利用 make 来创建一个 slice,可以像数组一样去取值,使用append来追加元素。 如果slice追加时,存储已满,就会立即扩容(一般为2倍),slice初始化也可以指定长度,不支持负数索引。

s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2])   // c
fmt.Println("len:", len(s)) // 3

s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]

c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]

fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5])  // [a b c d e]
fmt.Println(s[2:])  // [c d e f]

good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]

指针

Go也支持指针,但相比 C 与 C++ 的指针操作相对较少,支持的操作有限,不能进行指针移动,因此对内存的操作就比较安全,主要用途是对于传入参数进行修改,或者对比较大的对象进行传入地址,方便快捷。


package main

import "fmt"

func add2(n int) {
   n += 2
}

func add2ptr(n *int) {
   *n += 2
}

func main() {
   n := 5
   add2(n)
   fmt.Println(n) // 5
   add2ptr(&n)
   fmt.Println(n) // 7
}

结构体方法

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。上面的代码里那个附加的参数p,叫做方法的接收器(receiver),早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。

在Go语言中,我们并不会像其它语言那样用this或者self作为接收器;我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到,所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母,比如这里使用了User的首字母u。

package main

import "fmt"

type user struct {
   name     string
   password string
}

func (u user) checkPassword(password string) bool {
   return u.password == password
}

func (u *user) resetPassword(password string) {
   u.password = password
}

func main() {
   a := user{name: "wang", password: "1024"}
   a.resetPassword("2048")
   fmt.Println(a.checkPassword("2048")) // true
}

JSON 处理

对于一个已有的结构体,我们要保证结构体中的每个字段的首字母要大写,这样每个字段都可以包外访问,就能使用 JSON.marshaler进行序列化,变成一个字符串。

package main

import (
   "encoding/json"
   "fmt"
)

type userInfo struct {
   Name  string
   Age   int `json:"age"`
   Hobby []string
}

func main() {
   a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
   buf, err := json.Marshal(a)
   if err != nil {
      panic(err)
   }
   fmt.Println(buf)         // [123 34 78 97...]
   fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

   buf, err = json.MarshalIndent(a, "", "\t")
   if err != nil {
      panic(err)
   }
   fmt.Println(string(buf))

   var b userInfo
   err = json.Unmarshal(buf, &b)
   if err != nil {
      panic(err)
   }
   fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

三、实践练习例子:

随机数 Game

随机数处理

在我们生成随机数的时候之前,务必要设置随机数种子,不然每次都会生成相同的随机数序列,通常我们会使用当前的时间戳来处理,用 time.now.unix 来初始化种子Seed

maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)

核心代码细节

//标准io流处理
reader := bufio.NewReader(os.Stdin)
for {
    //读取cmd字符串
   input, err := reader.ReadString('\n')
   if err != nil {
      fmt.Println("An error occured while reading input. Please try again", err)
      continue
   }
   //截取字符串
   input = strings.TrimSuffix(input, "\n")
   //将字符串转换为int
   guess, err := strconv.Atoi(input)

爬虫字典项目

详细步骤

    1. 寻找翻译功能的请求的 Curl
    1. 再通过 Curl 生成模拟客户端请求 req 代码
    1. 发出模拟请求后,会返回 resp
    1. 接收到 resp,解析 resp.Body 为提前定义好的 dictResponse struct
    1. 再获取其中的 resp 想要的字段(提前定义好的 req)

Socks5 代理

Socks5 工作原理图 image.png 正常情况下的请求

即不经历代理服务器的情况下,首先要和服务器通过三次握手建立可靠连接,握手完再发送 HTTP 请求,然后服务器返回 HTTP 响应。

代理服务器下的请求

  • 第一阶段 —— 握手协商阶段

    浏览器会向 socks5 代理服务器发送请求,包的内容包括协议版本号支持认证种类, socks5 服务器会选中一个认证方式,返回给浏览器。

    如果返回的是 00 的话就代表不需要认证,如果是其他其他类型,则需要进行认证过程。

  • 第二阶段 —— 认证阶段

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)
   }
   log.Println("ver", ver, "method", method)
   // +----+--------+
   // |VER | METHOD |
   // +----+--------+
   // | 1  |   1    |
   // +----+--------+
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}
  • 第三阶段 —— 请求阶段

    认证通过了,之后浏览器就会发起请求,包主要信息包括版本号请求类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个IP地址的某个端口进行TCP连接,代理服务器收到响应后,会真正和后端服务器建立连接,然后返回一个响应。

  • 第四阶段 —— relay 阶段

    此时浏览器会发送正常的请求,然后代理服务器接收到请求之后,会直接把请求转换真正的服务器上,然后,如果真正的服务器以后响应的话,那么会把请求转发到代理服务器,再将请求转发到浏览器。

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])

  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()
  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)
  }
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()

  go func() {
     _, _ = io.Copy(dest, reader)
     cancel()
  }()
  go func() {
     _, _ = io.Copy(conn, dest)
     cancel()
  }()

  <-ctx.Done()
  return nil
}