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,然后再使用 reader 的 ReadString 方法读取标准输入的内容。这里需要我熟悉的是 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
这里又遇到了一个 deprecation。ioutil 包在 Go 1.16 之后不被鼓励继续使用,而应该使用 io 和 os 包中各自的实现。
大体上,使用 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
}
一开始,浏览器会向服务器发送含三个字段的报文以认证:
- VER: version of protocol, 0x05
- NMETHODS: number of methods
- 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
建权成功后即进入请求阶段。此阶段中,浏览器会向服务器发送包含六个字段的报文:
-
VER: version of protocol, 0x05
-
CMD: CONNECT request, 0x01
-
RSV: reserved field, 0x00
-
ATYP: type of target address
- 0x01 for IPv4; and
- 0x03 for Host.
-
DST.ADDR: variable length value according to the value of ATYP
-
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.