Go 基础语法和常用特性解析 | 青训营

79 阅读10分钟

前言

在本次青训营的前两节课中,对 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 中都是经过初始化的
  • 变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如: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!")
}

例程二:在线词典

这是一个简单的查词程序,通过调用在线翻译服务的接口,在网络上获取单词的释义。

同时介绍了两个实用工具站:

  1. curlconverter.com/ 可以将 curl 命令转换为各个语言的代码,避免了繁琐的输入请求头
  2. 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 在处理网络和并发时的便利性,总结如下:

  1. TCP服务器启动和连接处理:

    • 通过 net.Listen("tcp", ":1080") 启动一个TCP服务器,监听1080端口,用于接收客户端连接。
    • 使用 net.Accept() 接受客户端连接请求,并针对每个连接启动一个新的 goroutine 来处理。
  2. 缓冲读取数据:

    • 为了提高读取性能,代码使用了 bufio.NewReader() 来包装 net.Conn,以获得带缓冲的读取功能。这样可以减少系统调用的次数,从而提高读取效率。
  3. SOCKS5 协议认证部分:

    • auth 函数中,实现了 SOCKS5 协议的认证阶段。客户端连接后,服务器会返回支持的认证方法,并等待客户端选择认证方式。
  4. SOCKS5 协议连接请求处理部分:

    • connect 函数中,实现了 SOCKS5 协议的连接请求处理阶段。客户端会发送目标服务器的地址信息和要连接的命令(CONNECT/BIND/UDP)给代理服务器。
    • 代理服务器根据目标地址类型(IPV4/域名/IPV6)和端口信息,发起与目标服务器的连接,并将连接状态返回给客户端。
  5. 转发数据:

    • 一旦连接建立,代理服务器会将客户端的数据转发给目标服务器,同时将目标服务器的响应转发给客户端。这是通过两个 goroutine 来实现的,一个用于从客户端读取数据并写入目标服务器,另一个用于从目标服务器读取数据并写入客户端。
    • io.Copy() 从一个流拷贝数据到另一个流,直到遇到 EOF 或发生错误。
  6. Context 使用:

    • 使用 Go 的 context 包来创建一个上下文,用于在连接处理时进行控制和取消处理。在客户端断开连接时,取消上下文,以便终止 goroutine 中的数据转发。
  7. 大端字节序处理:

    • 在处理数据包中的数字信息(例如端口号)时,代码使用了 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 的优势,构建出高效、稳定且易于维护的应用程序。