GO语言工程实践课后作业:实现思路、代码以及路径记录(实践选题)|青训营

46 阅读13分钟

GO语言工程实践课后作业:实现思路、代码以及路径记录(实践选题)|青训营

go build指令的语法

go build [build flags] [packages]
常用的build flags
  • -o: 指定生成的可执行文件的输出路径和文件名。例如,go build -o myapp将生成名为myapp的可执行文件。
  • -ldflags: 设置链接时的标志。可以使用该标志传递自定义的链接标志,如版本信息、构建时间等。例如,go build -ldflags "-X main.version=1.0.0"将在可执行文件中设置main.version变量为1.0.0
  • -a: 强制重新编译所有依赖的包,而不仅仅是更新的包。
  • -v: 显示编译过程中的详细信息,包括编译的包和文件。

猜谜游戏

生成随机数

生成随机数需要用到math/rand软件包。

max := 100
rand.Seed(time.Now().UnixNano())
secretNum := rand.Intn(max)

rand.Intn的官方解释:Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n) from the default Source. It panics if n <= 0.

1. 介绍:

这是一个猜数字的小游戏。它生成一个随机的秘密数字,然后要求用户猜测这个数字,直到猜对为止。

2. 分析:

尽可能使代码的逻辑相对分散,可读性较好。

3. 思路:

  • 将生成秘密数字的部分封装成一个函数,提高代码的模块性和可维护性。
  • 将读取用户输入并转换为整数的逻辑封装成一个函数,以减少代码重复和提高可读性。
  • 将错误处理封装成函数,以避免重复的错误处理代码。

4. 作业代码:

将代码进行了分解,将不同功能封装成了不同的函数,以及添加了错误处理函数。这样,每个函数都有一个清晰的目的,提高了代码的可读性和可维护性。

  • generateSecretNumber 函数用于生成随机秘密数字。
  • readInput 函数用于读取用户输入并转换为整数。
  • main 函数是游戏的主逻辑,它调用了上述两个函数,更清晰地表达了游戏的流程。

以下是作业代码:

package main

import (
	"bufio"
	"fmt"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"time"
)

const maxNum = 100

func generateSecretNumber() int {
	rand.Seed(time.Now().UnixNano())
	return rand.Intn(maxNum)
}

func readInput(reader *bufio.Reader) (int, error) {
	input, err := reader.ReadString('\n')
	if err != nil {
		return 0, fmt.Errorf("an error occurred while reading input: %v", err)
	}
	input = strings.TrimSpace(input)

	guess, err := strconv.Atoi(input)
	if err != nil {
		return 0, fmt.Errorf("invalid input: please enter an integer value")
	}
	return guess, nil
}

func main() {
	secretNumber := generateSecretNumber()

	fmt.Println("Please input your guess")
	reader := bufio.NewReader(os.Stdin)
	for {
		guess, err := readInput(reader)
		if err != nil {
			fmt.Println(err)
			continue
		}

		fmt.Println("Your 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, Good Job!!!")
			break
		}
	}
}

一个issue

这个错误消息 "strconv.Atoi: parsing '12\r': invalid syntax" 表示在使用 strconv.Atoi() 将用户输入转换为整数时出现问题。

这个错误发生的原因是输入字符串中包含回车符 ('\r') 字符和换行符 ('\n') 字符。由于 strconv.Atoi() 期望一个不包含任何额外字符,如空格或换行符的有效整数字符串,所以这些特殊字符的存在导致了转换失败。

这个问题通常出现在从标准输入(键盘)读取输入时,在某些操作系统(如 Windows)上会出现。Windows 使用 '\r''\n' 字符来表示换行,而类Unix的系统(如 Linux、macOS)通常只使用 '\n'

为了解决这个问题,你可以修改删除尾部换行字符的代码,同时删除回车符(如果有的话)。一种方法是使用 strings.TrimSpace() 来替代 strings.TrimSuffix()

用这行代码替换:

input = strings.TrimSuffix(input, "\n")

使用这行代码:

input = strings.TrimSpace(input)

通过使用 strings.TrimSpace(),回车符和任何开头或结尾的空白字符都将被移除,只保留用户输入的整数字符串。这样应该能够解决在使用 strconv.Atoi() 进行转换时出现的 "invalid syntax" 错误。

在线词典

  • 需要通过F12的开发者工具来复制post命令的curl

遇到的问题

在这里复制到的curl没法在网站中解析:

原curl内容:

curl "https://api.interpreter.caiyunai.com/v1/dict" ^
  -H "authority: api.interpreter.caiyunai.com" ^
  -H "accept: application/json, text/plain, */*" ^
  -H "accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" ^
  -H "app-name: xy" ^
  -H "content-type: application/json;charset=UTF-8" ^
  -H "device-id: b8d6300e90ceab631564ace50d6c1c2d" ^
  -H "origin: https://fanyi.caiyunapp.com" ^
  -H "os-type: web" ^
  -H "os-version;" ^
  -H "referer: https://fanyi.caiyunapp.com/" ^
  -H "sec-ch-ua: ^\^"Not/A)Brand^\^";v=^\^"99^\^", ^\^"Microsoft Edge^\^";v=^\^"115^\^", ^\^"Chromium^\^";v=^\^"115^\^"" ^
  -H "sec-ch-ua-mobile: ?0" ^
  -H "sec-ch-ua-platform: ^\^"Windows^\^"" ^
  -H "sec-fetch-dest: empty" ^
  -H "sec-fetch-mode: cors" ^
  -H "sec-fetch-site: cross-site" ^
  -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188" ^
  -H "x-authorization: token:qgemv4jr1y38jyq6vhvi" ^
  --data-raw "^{^\^"trans_type^\^":^\^"en2zh^\^",^\^"source^\^":^\^"come^\^"^}" ^
  --compressed

调整后并且可被解析的curl:

curl "https://api.interpreter.caiyunai.com/v1/dict" \
  -H "authority: api.interpreter.caiyunai.com" \
  -H "accept: application/json, text/plain, */*" \
  -H "accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" \
  -H "app-name: xy" \
  -H "content-type: application/json;charset=UTF-8" \
  -H "device-id: b8d6300e90ceab631564ace50d6c1c2d" \
  -H "origin: https://fanyi.caiyunapp.com" \
  -H "os-type: web" \
  -H "os-version:" \
  -H "referer: https://fanyi.caiyunapp.com/" \
  -H "sec-ch-ua: \"^Not/A)Brand^\";v=\"99\", \"^Microsoft Edge^\";v=\"115\", \"^Chromium^\";v=\"115\"" \
  -H "sec-ch-ua-mobile: ?0" \
  -H "sec-ch-ua-platform: \"^Windows^\"" \
  -H "sec-fetch-dest: empty" \
  -H "sec-fetch-mode: cors" \
  -H "sec-fetch-site: cross-site" \
  -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188" \
  -H "x-authorization: token:qgemv4jr1y38jyq6vhvi" \
  --data-raw "{\"trans_type\":\"en2zh\",\"source\":\"come\"}" \
  --compressed

为什么要将curl请求转化为go代码

将 curl 请求转换为 Go 代码的一个常见原因是将命令行的操作转移到程序中,以便更灵活、可控地进行 HTTP 请求。以下是一些原因和优势:

  1. 集成性与模块化: 将 HTTP 请求代码集成到 Go 程序中可以使整个应用更加模块化和结构化。这样可以更方便地管理和组织代码。
  2. 类型安全: Go 是一门静态类型语言,这意味着在编译时可以捕获很多错误,如类型错误、函数参数错误等。这可以避免一些在运行时可能出现的错误。
  3. 错误处理: Go 的错误处理机制更加严格和清晰,有助于更好地处理异常情况,提供更好的用户体验和程序稳定性。
  4. 性能: Go 是一门编译型语言,通常比解释型语言更高效。通过将 HTTP 请求嵌入到 Go 代码中,可以提高执行效率。
  5. 跨平台支持: Go 可以轻松地构建跨平台的应用,您可以在不同操作系统上运行相同的代码。
  6. 可维护性: Go 代码通常比较干净、易读,遵循一致的编码风格。这有助于提高代码的可维护性。
  7. 代码版本控制: 将 HTTP 请求转换为 Go 代码后,可以方便地将这些代码与应用的其他部分一起纳入版本控制,从而更好地管理和跟踪代码的变化。
  8. 自动化测试: 将 HTTP 请求嵌入到 Go 测试中,可以更轻松地进行单元测试和自动化测试,从而提高代码质量。

总的来说,将 curl 请求转换为 Go 代码可以提供更多的控制和灵活性,同时也有助于提高代码质量、性能和可维护性。这不仅适用于简单的请求,还可以用于复杂的网络操作和集成。

生成的代码

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`{"trans_type":"en2zh","source":"come"}`)
	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("accept", "application/json, text/plain, */*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("device-id", "b8d6300e90ceab631564ace50d6c1c2d")
	req.Header.Set("origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("os-type", "web")
	req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("sec-ch-ua", `"^Not/A)Brand^";v="99", "^Microsoft Edge^";v="115", "^Chromium^";v="115"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"^Windows^"`)
	req.Header.Set("sec-fetch-dest", "empty")
	req.Header.Set("sec-fetch-mode", "cors")
	req.Header.Set("sec-fetch-site", "cross-site")
	req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188")
	req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", bodyText)
}

  1. 创建一个 http.Client 对象,用于发送 HTTP 请求:

    client := &http.Client{}
    
  2. 创建一个字符串作为请求的数据,使用 strings.NewReader 函数将 JSON 数据包装成 io.Reader 接口:

    var data = strings.NewReader(`{"trans_type":"en2zh","source":"come"}`)
    
  3. 创建一个 http.Request 对象,表示要发送的请求。设置请求方法为 POST,URL 为目标 API 的地址,请求数据为之前创建的 data 变量:

    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
    
  4. 设置请求头,包括各种自定义的头部信息:

    req.Header.Set("authority", "api.interpreter.caiyunai.com")
    req.Header.Set("accept", "application/json, text/plain, */*")
    // ... 设置其他头部信息 ...
    
  5. 使用客户端执行请求,并获取响应对象:

    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    
  6. 从响应的 Body 属性中读取返回的数据,使用 io.ReadAll 函数将响应内容读取到 bodyText 变量中:

    bodyText, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    
  7. 打印响应内容到控制台:

    fmt.Printf("%s\n", bodyText)
    

这段代码的目的是向 https://api.interpreter.caiyunai.com/v1/dict 发送一个 POST 请求,包含指定的请求头和 JSON 数据,然后获取响应并将其输出到控制台。

作业代码

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"

	"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 {
	} `json:"wiki"`
	Dictionary struct {
		Prons struct {
			EnUs string `json:"en-us"`
			En   string `json:"en"`
		} `json:"prons"`
		Antonym      []string      `json:"antonym"`
		Entry        string        `json:"entry"`
		Explanations []string      `json:"explanations"`
		Source       string        `json:"source"`
		Synonym      []interface{} `json:"synonym"`
		Type         string        `json:"type"`
		WqxExample   [][]string    `json:"wqx_example"`
	} `json:"dictionary"`
}

func query(word string) {
	client := &http.Client{}
	request := DictRequest{TransType: "en2zh", Source: word}
	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("accept", "application/json, text/plain, */*")
	req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("device-id", "b8d6300e90ceab631564ace50d6c1c2d")
	req.Header.Set("origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("os-type", "web")
	req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("sec-ch-ua", `"^Not/A)Brand^";v="99", "^Microsoft Edge^";v="115", "^Chromium^";v="115"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"^Windows^"`)
	req.Header.Set("sec-fetch-dest", "empty")
	req.Header.Set("sec-fetch-mode", "cors")
	req.Header.Set("sec-fetch-site", "cross-site")
	req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188")
	req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
	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
	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() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)
}

  1. 定义结构体 DictRequestDictResponse 来映射请求和响应的 JSON 数据结构。
  2. 定义了一个 query 函数,用于发起词典查询请求。该函数接受一个单词作为参数。
  3. query 函数内部,创建了一个 HTTP 客户端,并使用 DictRequest 结构体构建查询请求的 JSON 数据。
  4. 使用 http.NewRequest 函数创建一个 POST 请求,设置 URL 和请求数据。然后设置请求头,包括各种自定义的头部信息。
  5. 执行请求,获取响应对象。如果发生错误,将会打印错误并退出。
  6. 关闭响应的 Body。
  7. 读取响应的内容,将其解析为字节数组。
  8. 检查响应的状态码,如果不是 200,则打印错误信息并退出。
  9. 使用 json.Unmarshal 函数将响应内容解析为 DictResponse 结构体,以便后续访问和处理。
  10. 打印单词的英文发音和中文释义。
  11. main 函数中,检查命令行参数的数量。如果不是 2 个参数(包括程序名和要查询的单词),则输出用法信息并退出。
  12. 获取要查询的单词,调用 query 函数执行查询操作。

这段代码实现了一个简单的词典查询工具,可以在命令行中输入要查询的英文单词,然后通过调用 API 获取该单词的中文释义和发音。

SOCKS5 代理服务器

这段代码实现了一个简单的 SOCKS5 代理服务器,用于接受客户端连接并进行代理转发。下面是代码的实现思路:

  1. 导入所需的包:
    • bufio:用于缓冲输入输出。
    • context:用于控制并发操作的上下文管理。
    • encoding/binary:用于处理二进制数据。
    • errors:用于表示错误。
    • fmt:用于格式化输出。
    • io:用于输入输出操作。
    • log:用于日志记录。
    • net:用于网络通信。
  2. 定义一些常量:
    • socks5Ver:SOCKS5 协议版本。
    • cmdBind:代表 CONNECT 请求的命令值。
    • atypeIPV4atypeHOSTatypeIPV6:不同类型的地址。
  3. 当看代码时,了解每个函数的功能和作用非常重要。以下是代码中每个函数的详细介绍:
    1. main 函数:
      • 这是程序的入口函数,用于启动 SOCKS5 代理服务器并监听客户端连接。
      • 创建一个 TCP 服务器,监听在本地的 1080 端口上。
      • 循环接受客户端连接,对每个客户端连接启动一个 process 协程进行处理。
    2. process 函数:
      • 用于处理客户端连接,包括身份验证和连接请求处理。
      • 参数 conn 是客户端连接的 net.Conn 对象。
      • 调用 auth 函数进行身份验证,如果验证失败则关闭连接。
      • 调用 connect 函数处理连接请求,如果处理失败则关闭连接。
    3. auth 函数:
      • 进行 SOCKS5 协议的身份验证阶段。
      • 参数 reader 是从客户端连接中创建的 bufio.Reader 对象。
      • 参数 conn 是客户端连接的 net.Conn 对象。
      • 读取协议版本和支持的认证方法数量,验证协议版本为 SOCKS5,并返回支持的认证方法。
      • 向客户端写入协议版本和选择的认证方法。
    4. connect 函数:
      • 处理 CONNECT 请求阶段,建立与目标服务器的连接。
      • 参数 reader 是从客户端连接中创建的 bufio.Reader 对象。
      • 参数 conn 是客户端连接的 net.Conn 对象。
      • 解析 CONNECT 请求头,获取目标服务器地址和端口。
      • 根据地址和端口建立与目标服务器的连接。
      • 向客户端写入 CONNECT 响应头,表示已建立连接。
    5. func main() 中的协程:
      • 在数据拷贝协程中使用 context 控制操作的取消,以确保在连接关闭或出现错误时及时终止数据拷贝。
      • 启动两个协程,一个从客户端到目标服务器进行数据拷贝,另一个从目标服务器到客户端进行数据拷贝。
      • 使用 io.Copy 函数进行数据拷贝,将一个 io.Reader 的数据拷贝到一个 io.Writer
  4. 在数据拷贝协程中使用 context 控制操作的取消,以确保在连接关闭或出现错误时及时终止数据拷贝。
  5. 代码结构简单明了,基于 SOCKS5 协议规范实现了基本的代理服务器功能。

实现代码

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

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.Printf("Accept failed %v", err)
			continue
		}
		go process(client)
	}
}

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
	}
}

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
}

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", cmd)
	}
	addr := ""
	switch atyp {
	case atypeIPV4:
		_, 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
}