前言
在本次青训营的前两节课中,对 Golang 进行了快速介绍和基础语法快速入门,通过结合学习与实践,还编写了三个示例程序。
本文结合笔者的学习理解和课堂所学,对部分重点知识细节进行了总结,并完成了对示例程序的代码修改和优化。
重要基础知识细节
-
package name: Go 以包作为管理单位,每个 Go 源文件必须先声明它所属的包- 一个目录下的同级文件属于同一个包
- 包名可以与其目录名不同
- main 包是 Go 程序的入口包,一个 Go 程序有且仅有一个 main 包
-
import "name": 也可以使用一个 import 关键字导入多个包,此时需要用括号( )将包的名字包围起来,且每个包名占用一行- 导入的包中不能含有代码中没有使用到的包,否则 Go 编译器会报编译错误
func 函数名 (参数列表) (返回值列表){
函数体
}
注意:Go 函数的左花括号 { 必须和函数名称在同一行,否则会报错
-
当一个变量被声明之后,系统自动赋予它该类型的零值
- int 为
0,float 为0.0,bool 为false,string 为空字符串,指针为nil等 - 所有的内存在 Go 中都是经过初始化的
- int 为
-
变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate
我们可以发现,在 Go 中还有一个与其他大多数语言不同的重要区别,即变量类型后置。查阅资料可知,这样做可以避免出现类似 C 语言 int* a, b; 易混淆的声明。
Go 中还可以以 name := exp 的格式进行简短变量声明,但左值中必须包含至少一个未定义的变量,否则会发生编译错误。
Go 语言中的函数通常会将 Error 作为第二个返回值返回,而不是像其他语言一样抛出异常,你需要检查 err 是否为 nil 判断是否发生错误。
例程
例程一:猜数字
本例程是一个简单的基础程序:生成一个随机数,接收用户的输入,判断大于小于还是等于,猜错继续接收用户输入。
在课程中,使用了 bufio.NewReader(os.Stdin) 的方式建立了一个带缓冲的读取器,读取标准输入。我们还可以使用 fmt 包中提供的 Scan、Scanf 等函数直接读取,例如 fmt.Scan(&guessNum) 可以直接扫描一个数存储到指定变量中。
此外,课程中还介绍了 rand.Seed(time.Now().UnixNano()) 设置全局随机数种子的方法,但根据提案( github.com/golang/go/i… ),在 Go 1.20 中,该方法已经废弃,现在你可以直接获取伪随机数,无需手动设置种子,它在每次运行时也不相同。如果一定要固定随机数生成的结果,可以使用 rand.New(rand.NewSource(seed)) 获取一个指定种子的随机数生成器。
经过优化的代码如下:
package main
import (
"fmt"
"math/rand"
)
func main() {
maxNum := 100
// 1.20版本开始rand.Seed函数已被弃用,因为任何包都可能调用它
// rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
fmt.Println("Guess a number between 0 and", maxNum)
fmt.Print("Please input your guess number: ")
var guessNum int
fmt.Scan(&guessNum)
for guessNum != secretNumber {
if guessNum > secretNumber {
fmt.Print("Too big, please try again: ")
} else {
fmt.Print("Too small, please try again: ")
}
fmt.Scan(&guessNum)
}
fmt.Println("You win!")
}
例程二:在线词典
这是一个简单的查词程序,通过调用在线翻译服务的接口,在网络上获取单词的释义。
同时介绍了两个实用工具站:
- curlconverter.com/ 可以将 curl 命令转换为各个语言的代码,避免了繁琐的输入请求头
- oktools.net/json2go 将 JSON 自动解析为 Go 结构体代码
在这个例子中也介绍了 Go 中读取运行参数、序列和反序列化 JSON 等操作。可以看到在 Golang 中,可以通过 json.Marshal() 和 json.Unmarshal() 轻松将 JSON 与结构体类型相互转换,在处理网络通信时非常方便。
以下是增加了翻译引擎并使用协程完成并行请求的代码:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
)
type CaiyunRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
}
type CaiyunResponse struct {
Rc int `json:"rc"`
Wiki struct {
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []string `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
type YoudaoResponse struct {
Ec struct {
WebTrans []string `json:"web_trans"`
ExamType []string `json:"exam_type"`
Source struct {
Name string `json:"name"`
URL string `json:"url"`
} `json:"source"`
Word struct {
Usphone string `json:"usphone"`
Ukphone string `json:"ukphone"`
Ukspeech string `json:"ukspeech"`
Trs []struct {
Pos string `json:"pos"`
Tran string `json:"tran"`
} `json:"trs"`
ReturnPhrase string `json:"return-phrase"`
Usspeech string `json:"usspeech"`
} `json:"word"`
} `json:"ec"`
}
var wg sync.WaitGroup
func main() {
var word string
if len(os.Args) > 1 {
word = os.Args[1]
} else {
fmt.Print("请输入要查询的单词:")
fmt.Scanln(&word)
}
//queryCaiyun(word)
//queryYoudao(word)
wg.Add(2)
go func() {
res := queryCaiyun(word)
log.Print(res)
wg.Done()
}()
go func() {
res := queryYoudao(word)
log.Print(res)
wg.Done()
}()
wg.Wait()
}
func queryYoudao(word string) (result string) {
client := &http.Client{}
var data = strings.NewReader(`q=` + word + `&le=en&t=9&client=web&keyfrom=webdict`)
req, err := http.NewRequest("POST", "https://dict.youdao.com/jsonapi_s?doctype=json&jsonversion=4", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("DNT", "1")
req.Header.Set("Origin", "https://www.youdao.com")
req.Header.Set("Referer", "https://www.youdao.com/")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "same-site")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
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-gpc", "1")
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)
}
if resp.StatusCode != 200 {
log.Fatal("bad status code:", resp.StatusCode, " ", string(bodyText))
}
var dictResponse YoudaoResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
builder := strings.Builder{}
builder.WriteString("[有道词典]\n")
builder.WriteString(word)
builder.WriteString(" UK:")
builder.WriteString("[" + dictResponse.Ec.Word.Ukphone + "]")
builder.WriteString(" US:")
builder.WriteString("[" + dictResponse.Ec.Word.Usphone + "]\n")
for _, tr := range dictResponse.Ec.Word.Trs {
builder.WriteString(tr.Pos)
builder.WriteString(" ")
builder.WriteString(tr.Tran + "\n")
}
return builder.String()
}
func queryCaiyun(word string) (result string) {
client := &http.Client{}
request := CaiyunRequest{
TransType: "en2zh",
Source: word,
}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf) // data是一个流
//var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`) // data是一个流
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
setHeader(req)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // defer是延迟执行,会在函数结束时从下往上执行
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != 200 {
log.Fatal("bad status code:", resp.StatusCode, " ", string(bodyText))
}
var dictResponse CaiyunResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
builder := strings.Builder{}
builder.WriteString("[彩云词典]\n")
builder.WriteString(word)
builder.WriteString(" UK:")
builder.WriteString(dictResponse.Dictionary.Prons.En)
builder.WriteString(" US:")
builder.WriteString(dictResponse.Dictionary.Prons.EnUs + "\n")
for _, v := range dictResponse.Dictionary.Explanations {
builder.WriteString(v + "\n")
}
return builder.String()
}
func setHeader(req *http.Request) {
(*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")
(*req).Header.Set("app-name", "xy")
(*req).Header.Set("content-type", "application/json;charset=UTF-8")
(*req).Header.Set("dnt", "1")
(*req).Header.Set("origin", "https://fanyi.caiyunapp.com")
(*req).Header.Set("os-type", "web")
(*req).Header.Set("os-version", "")
(*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("sec-gpc", "1")
(*req).Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
(*req).Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
}
代码中使用了 sync.WaitGroup 等待协程结束,sync.WaitGroup 中有一个计数器,即为需要等待的协程数量,初始为0,调用 wg.Add() 可以增加计数器计数。
在协程中,完成执行后调用 wg.Done() 将使计数器减一;wg.Wait() 将阻塞,直到计数器归零。
在定义反序列化所用结构体时,可以省略代码中用不到的部分。
例程三:Socks5代理服务器
本例是最有趣的例程之一,实现了一个简单的 SOCKS5 代理服务器,能够接收来自客户端的连接请求,并将请求转发给目标服务器,同时将目标服务器的响应返回给客户端,实现了基本的代理功能。代码中涉及的技术点包括 TCP服务器的启动、缓冲读取数据、解析 SOCKS5 协议认证和连接请求、数据转发、使用 Context 控制并发等。可以很好的体现 Golang 在处理网络和并发时的便利性,总结如下:
-
TCP服务器启动和连接处理:
- 通过
net.Listen("tcp", ":1080")启动一个TCP服务器,监听1080端口,用于接收客户端连接。 - 使用
net.Accept()接受客户端连接请求,并针对每个连接启动一个新的 goroutine 来处理。
- 通过
-
缓冲读取数据:
- 为了提高读取性能,代码使用了
bufio.NewReader()来包装net.Conn,以获得带缓冲的读取功能。这样可以减少系统调用的次数,从而提高读取效率。
- 为了提高读取性能,代码使用了
-
SOCKS5 协议认证部分:
- 在
auth函数中,实现了 SOCKS5 协议的认证阶段。客户端连接后,服务器会返回支持的认证方法,并等待客户端选择认证方式。
- 在
-
SOCKS5 协议连接请求处理部分:
- 在
connect函数中,实现了 SOCKS5 协议的连接请求处理阶段。客户端会发送目标服务器的地址信息和要连接的命令(CONNECT/BIND/UDP)给代理服务器。 - 代理服务器根据目标地址类型(IPV4/域名/IPV6)和端口信息,发起与目标服务器的连接,并将连接状态返回给客户端。
- 在
-
转发数据:
- 一旦连接建立,代理服务器会将客户端的数据转发给目标服务器,同时将目标服务器的响应转发给客户端。这是通过两个 goroutine 来实现的,一个用于从客户端读取数据并写入目标服务器,另一个用于从目标服务器读取数据并写入客户端。
io.Copy()从一个流拷贝数据到另一个流,直到遇到 EOF 或发生错误。
-
Context 使用:
- 使用 Go 的
context包来创建一个上下文,用于在连接处理时进行控制和取消处理。在客户端断开连接时,取消上下文,以便终止 goroutine 中的数据转发。
- 使用 Go 的
-
大端字节序处理:
- 在处理数据包中的数字信息(例如端口号)时,代码使用了
binary.BigEndian来进行大端字节序的处理,以确保在不同系统上的正确解析。
- 在处理数据包中的数字信息(例如端口号)时,代码使用了
package main
import (
"bufio"
"context"
"encoding/binary"
"fmt"
"io"
"log"
"net"
)
func main() {
listen, err := net.Listen("tcp", ":1080")
if err != nil {
panic(err)
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
log.Printf("accept error: %v", err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn) // 只读带缓冲的流
err := auth(reader, conn)
if err != nil {
log.Printf("%v auth error: %v", conn.RemoteAddr(), err)
return
}
log.Printf("%v auth success", conn.RemoteAddr())
err = connect(reader, conn)
if err != nil {
log.Printf("%v connect error: %v", conn.RemoteAddr(), err)
return
}
}
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read version error: %v", err)
}
if ver != socks5Ver {
return fmt.Errorf("version error: %v", ver)
}
nmethods, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read nmethods error: %v", err)
}
methods := make([]byte, nmethods)
_, err = io.ReadFull(reader, methods)
if err != nil {
return fmt.Errorf("read methods error: %v", err)
}
log.Println("version:", ver, "nmethods:", nmethods, "methods:", methods)
// 不需要认证
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write auth response error: %v", err)
}
return nil
}
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read connect header error: %v", err)
}
ver, cmd, atype := buf[0], buf[1], buf[3]
if ver != socks5Ver {
return fmt.Errorf("version error: %v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("cmd error: %v", cmd)
}
var host string
switch atype {
case atypeIPV4:
_, err := io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read ipv4 error: %v", err)
}
host = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
hostLen, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read host len error: %v", err)
}
hostBuf := make([]byte, hostLen)
_, err = io.ReadFull(reader, hostBuf)
if err != nil {
return fmt.Errorf("read host error: %v", err)
}
host = string(hostBuf)
case atypeIPV6:
ipv6 := make([]byte, 16)
_, err := io.ReadFull(reader, ipv6)
if err != nil {
return fmt.Errorf("read ipv6 error: %v", err)
}
host = net.IP(ipv6).String()
host = fmt.Sprintf("[%v]", host)
default:
return fmt.Errorf("atype error: %v", atype)
}
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port error: %v", err)
}
//port := int(buf[0])<<8 + int(buf[1])
port := binary.BigEndian.Uint16(buf[:2])
// 建立到目标服务器的连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", host, port))
if err != nil {
return fmt.Errorf("dial error: %v", err)
}
defer dest.Close()
log.Println("dial", host, port)
_, err = conn.Write([]byte{socks5Ver, 0x00, 0x00, atypeIPV4, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write connect response error: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 进行转发
go func() {
_, _ = io.Copy(dest, reader)
}()
go func() {
_, _ = io.Copy(conn, dest)
}()
<-ctx.Done()
return nil
}
后记
通过这段时间的学习,可以看出 Golang 确实是非常方便的语言,代码清晰易读、标准库强大。其中,笔者最喜欢的 Go 的特性是静态编译以及无需庞大的 RUNTIME 环境,可以轻松分发和部署。
Golang 在语言级别内置了轻量级的协程(goroutine)和通道(channel)机制,使得并发编程变得非常简单。让处理并发任务变得容易,在多核处理器上能够充分发挥性能。
通过不断地学习和实践,我们可以更好地利用 Golang 的优势,构建出高效、稳定且易于维护的应用程序。