Go 语言第一课 | 青训营

55 阅读5分钟

Go 语言基础语法

Tue, Jul 25, 2023

In many cases, error handlings are omitted for the sake of simplicity.


1 Basic syntax of Go

The point where Go most impressed me is its error handling. Go encourages us to explicitly check for errors and handle them. You see if err != nil { ... } all the time.

2 Three little projects

2.1 Guessing game

2.1.1 Set random seed (Legacy)

课程中讲 v1 因为没有设置随机数生成种子而每次生成相同的数,但我在跑的时候发现能够正常生成不同的随机数。Goland reminded me that rand.Seed() is already deprecated. After Go 1.20, the generator is randomly seeded at program startup.

Calling rand.Seed(1) at program startup, I got the expected behavior where I get the same number every run.

2.1.2 Take user input

尽管 Go 中存在 Scanf(),但为了演示 bufio 库的使用,lecturer 选择了一种较为复杂的读取用户输入的方法。

reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')

首先获取 Stdin 文件地址并创建一个 reader,然后再使用 readerReadString 方法读取标准输入的内容。这里需要我熟悉的是 Go 的错误处理。

if err != nil {
    fmt.Println("An error occured while reading input. Please try again", err)
    return
}

Other languages normally throw exceptions and sometimes catch them. But Go encourages developers to explicitly check for errors when they occur.

2.2 Command line dictionary

2.2.1 Create a request

这里又遇到了一个 deprecationioutil 包在 Go 1.16 之后不被鼓励继续使用,而应该使用 ioos 包中各自的实现。

大体上,使用 net/http 包进行 http 请求并获取返回值的流程如下:

client := &http.Client()
req, err := http.NewRequest(method, url, body)
req.Header.Set(key, value)
resp := client.Do(req)
defer resp.Body.Close()
bodyText, err := io.ReadAll(resp.Body)

Here, goland warns me to wrap the Close method in a closure as below:

defer func(Body io.ReadCloser) {
    err := Body.Close()
    if err != nil {
        
    }
}(resp.Body)

2.2.2 JSON Serialization

The problem is, currently the input is fixed. We need to take user input by a variable rather than a JSON string, after all. Therefore, we've got to serialize our input from the previous steps.

type DictRequest struct {
    TransType string `json:"trans_type"`
    Source    string `json:"source"`
    UserID    string `json:"user_id"`
}

Firstly, we define a structure whose fields match the JSON counterpart respectively.

Secondly, we create a request of DictRequest type. Next, call json.Marshal() to encode our request to a JSON bytes array.

Now that we have serialized our input, we need to deserialize the output, on the other hand.

Similarly, we define a structure that matches the output JSON fields. Then, with json.Unmarshall() we deserialize the JSON response into an iterable.

type DictResponse struct { ... }
// large structure generated by online tool
var v DictResponse
err = json.Unmarshall(data, v)

2.2.3 Defensive programming

if resp.StatusCode != 200 {
    log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}

Check the status code of the response to avoid generating empty output later.

2.3 Socks5 Proxy

2.3.1 TCP echo server

TCP echo server is an in-memory web server that echoes back the arguments given to it.

server, err := net.Listen("tcp", "127.0.0.1:1080")
for {
    client, err := server.Accept()
    go process(client) // process defined below
}

首先,监听本地 1080 端口,返回一个 net.Listener 类型的变量 server

接着,在一个死循环中,Listener.Accept() 等待并返回下一个连接 (net.Conn)client。然后创建一个 goroutine 子进程,并发处理不同连接。

func process(client net.Conn) {
    defer client.Close()
    reader := bufio.NewReader(client)
    for {
        b, err := reader.ReadByte()
        _, err = client.Write([]byte{b})
    }
}

在每个 goroutine 中,基于 client 构建一个 read-only buffered stream。

接着,在一个死循环中,每次读取并向 client 中写入一个子节。

此 goroutine 退出时,执行 defer client.Close() 关闭连接。

2.3.2 Authentication

上一步中,TCP echo server 确保了正常的连接。接下来,创建一个 auth 函数建权。

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
    ver, err := reader.ReadByte()
    if ver != socks5Ver {
        // const socks5Ver = 0x05
        return fmt.Errorf("not supported ver:%v", ver)
    }
    methodSize, err := reader.ReadByte()
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    log.Println("ver", ver, "method", method)
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    return nil
}

一开始,浏览器会向服务器发送含三个字段的报文以认证:

  1. VER: version of protocol, 0x05
  2. NMETHODS: number of methods
  3. METHODS: code of methods

使用 reader.ReadByte() 从流中读取一个子节,存入 ver。同样地,再读一个子节存入 methodSize

内置函数 make() 创建一个 methodSize 大小的 byte array 作 buffer。接着,io.ReadFull() 填满此 buffer。

这样,我们就获得了认证需要的全部三个字段,接下来直接创建一个回报:[]byte{socks5Ver, 0x00} 并使用 conn.Write() 发送给服务器。

process 函数中,将死循环改为调用 auth 函数,若返回错误则打印错误信息并返回:

err := auth(reader, conn)
if err != nil {
    log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
    return
}

2.3.3 Make a request

建权成功后即进入请求阶段。此阶段中,浏览器会向服务器发送包含六个字段的报文:

  1. VER: version of protocol, 0x05

  2. CMD: CONNECT request, 0x01

  3. RSV: reserved field, 0x00

  4. ATYP: type of target address

    • 0x01 for IPv4; and
    • 0x03 for Host.
  5. DST.ADDR: variable length value according to the value of ATYP

  6. DST.PORT: target port, 2 bytes

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    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)
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    case atypeHOST:
        hostSize, err := reader.ReadByte()
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        addr = string(host)
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    
    _, err = io.ReadFull(reader, buf[:2])
    port := binary.BigEndian.Uint16(but[:2])
    
    log.Println("dial", addr, port)
    
    _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    return nil
}

Firstly, we create a 4-byte buffer filled by io.ReadFull() to read the first four fields and check for legality.

Secondly, according to the value of atyp, we read address differently.

For IPv4, we read the next four bytes and print into string addr. And for Host, we read one byte to get the length of the host. Next, according to the host size, we make a buffer and fill it. And finally, we stringify it and assign to addr.

Thirdly, we read the final port field into a two-byte slice of the buffer created above. Then we parse the data into uint16 type in big endian byte ordering by using function providing by encoding/binary.

Finally, we send a response message.

2.3.4 Relay

与 IP 端口建立连接,双向转换数值。

dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
defer dest.Close()
log.Println("dial", addr, port)

首先,使用 net.Dial 函数创建一个 TCP 连接。

标准库中提供了 io.Copy() 以实现单向数值转换。我们可以创建两个 goroutine 来完成双向数值转换。

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

Because creating a goroutine almost doesn't consume time, our code runs return nil almost immediately. To return and terminate the connection when either party closes the connection, we use context from the standard library.