Go语言入门的实战案例 | 青训营

120 阅读4分钟

猜数字

使用bufio流式处理来自stdin的输入

reader := bufio.NewReader(os.stdin)
str, err := reader.ReadString('\n')

strconv包的Atoi(string) int方法将字符串转为数字,在此之前用stringsTrimSuffix(string, suffix) string方法把字符串末尾的换行"\n"删除(CRLF是"\r\n"

num, err := strconv.Atoi(strings.TrimSuffix(str, "\n"))

就完成了读入数字的操作剩下的就是写点分支不做赘述

命令行词典

主要的收获是几个好用的网站,以前一直是手动造各种请求头,手动处理返回的json,麻烦又容易出错,一直没想去找点工具。 这里主要是把生成的代码再过一遍,依赖工具的同时不能忘了怎么手写。

新建一个http.Client

client := &http.Client{}

初始化请求body并且进行序列化

request := DictRequest{
        TransType: "en2zh",
        Source:    word,
}
buf, err := json.Marshal(request)

用buf创建一个流,并且新建请求

data := bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

设置请求头

req.Header.Set("authority", "api.interpreter.caiyunai.com")
req.Header.Set("accept", "application/json, text/plain, */*")
// ... 后面省略一万个请求头

client.Do(req)发起请求

resp, err := client.Do(req)

别忘了加上defer

defer resp.Body.Close()

然后就可以获得返回的body文本

bodyText, err := io.ReadAll(resp.Body)

之前的包括这里检查err的代码都省略了,在此处还需要注意status code是否为200,因为有可能不为200返回的body是错误的数据

反序列化字典响应

var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)

之后简单打印一下就ok了

可以命令行传入参数,通过os.Args来获取并且依次翻译,运行如下

image.png

socks5 代理服务器

首先要监听一个端口,不断用goroutine处理来自客户端的连接

server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
        panic(err)
}
for {
        conn, err := server.Accept() // 等待一个客户端的连接
        if err != nil {
                log.Fatal("Accept failed:", err) // 打印错误信息
                continue
        }
        go handle(conn) // 处理客户端连接
}

对于handle方法,主要的逻辑是:先读入请求的头进行验证,然后再获取连接的具体内容建立双向的读写,简要的代码如下

func handle(conn net.Conn) {
  defer conn.Close() // 延迟关闭conn

  reader := bufio.NewReader(conn) // 读入流
  err := auth(reader, conn)       // 验证
  ... // err

  err = connect(reader, conn) // 连接
  ... // err
}

auth是读入ver, methodSize, method三个部分,分别对应版本、方法大小、方法,其中前两个都是一字节,最后一个method长度是methodSize指定的。用readByte() (byte, error)方法读入字节后检查err然后还要核验版本号的合法性。读入方法的时候先make([]byte, methodSize)创建一个长度为methodSize的缓冲区,然后io.ReadFull一次性读完。

/* 验证请求头 */
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
  // 版本号
  ver, err := reader.ReadByte()
  if err != nil {
    return fmt.Errorf("read header failed: %v", err)
  }
  if ver != socks5Ver {...}

  // 方法大小
  size, err := reader.ReadByte()
  if err != nil {...}

  // 方法
  method := make([]byte, size)
  _, err = io.ReadFull(reader, method)
  if err != nil {...}
  
  // 写回response
  _, err = conn.Write([]byte{ver, 0x00})
  if err != nil {...}

  return nil
}

连接部分在前面的读取部分几乎是一样的,要读ver,cmd,rsv,atype四个字节的头(对应版本、指令、保留字、地址类型),可以创建一个四字节的缓冲区buf一次性读入,核验完版本和指令之后,根据不同的atype,对应的获取实际的地址,其中0x01是ipv4,后跟四个字节,0x03是不定长的域名,后跟不定长的字符串,同样的第一个字节是数据长度,读入后再读该长度的就是实际的地址。最后读入两字节的port,可以用切片buf[:2]复用buf,然后通过binary.BigEndian.Uint16(buf[:2])获取到port。

在进行了上述操作之后,我们会得到地址addr,端口port。之后用net.Dial建立和实际服务器的连接。成功后给客户端返回一个响应。我们使用context库来等待双向连接结束,在读写错误时直接退出。

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
  ...
  // 建立连接
  dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
  if err != nil {
    return fmt.Errorf("dial %v:%v failed: %v", addr, port, err)
  }
  defer dest.Close()

  // 写回response
  _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
  if err != nil {...}

  // 创建一个context,当cancel被调用时,ctx.Done才会触发
  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
}

最后实际的运行,设置浏览器代理后访问pkg.go.dev/

image.png