Go基础语法(实战)

94 阅读13分钟

猜谜游戏

首先会生成一个介于1到100之间的随机整数,然后提示玩家进行猜测。玩家每次输入一个数字,程序会告诉玩家这个猜测的值是高于还是低于那个秘密的随机数,并且让玩家再次猜测。如果猜对了,就告诉玩家胜利并且退出程序。

package main

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

func main() {
	//生成随机数
	maxNum := 100
	rand.Seed(time.Now().UnixNano()) //设置随机数种子(但好像不设也能保证每次生成的数字不一样)
	secretNumber := rand.Intn(maxNum)
	fmt.Println("待猜测数字为:", secretNumber)

	//实现用户输入并解析成数字
	/*
		每个程序执行的时候都会打开几个文件,stdin stdout stderr等,stdin文件可以用os.Stdin来得到。
		直接操作这个文件很不方便,用bufio.NewReader把一个文件转换成一个reader变量。
		reader变量上会有很多用来操作一个流的操作,可以用它的ReadString方法来读取一行。
		如果失败会打印错误并能退出。
		ReadString返回的结果包含结尾的换行符,把它去掉,再转换成数字。如果转换失败,同样打印错误,退出。
	*/
	fmt.Println("输入你的猜测:")
	reader := bufio.NewReader(os.Stdin)

	//input, err := reader.ReadString('\n') //读取一行输入
	//
	//if err != nil {
	//	fmt.Println("数据读取发生错误", err)
	//	return
	//}
	//input = strings.TrimSuffix(input, "\n") //去掉换行符
	//
	//guess, err := strconv.Atoi(input)
	//if err != nil {
	//	fmt.Println("无效输入,请重新输入")
	//	//return
	//}
	//
	//fmt.Println("你的猜测:", guess)

	//判断逻辑
	for {
		input, err := reader.ReadString('\n') //读取一行输入

		if err != nil {
			fmt.Println("数据读取发生错误", err)
			continue
			//return
		}
		input = strings.TrimSuffix(input, "\n") //去掉换行符

		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("无效输入,请重新输入")
			continue
			//return
		}

		fmt.Println("你的猜测:", guess)

		if guess > secretNumber {
			fmt.Println("猜大了,往小了猜~!")
		} else if guess < secretNumber {
			fmt.Println("猜小了,往大了猜~!")
		} else {
			fmt.Println("对喽!!!")
			break
		}
	}

}

在线词典

用户可以在命令行里面查询一个单词。通过调用第三方的AP!查询到单词的翻译并打印出来。

学习如何用go语言来来发送HTTP请求、解析json过来,还会学习如何使用代码生成来提高开发效率。

以彩云科技提供的在线翻译为例。先打开彩云翻译的网页,然后右键检查打开浏览器的开发者工具。

请求头是一个json里面有两个字段,一个是代表你要你是从什么语言转化成什么语言,source就是你要查询的单词。API的返回结果里面会有Wk和dictionary两个字段。我们需要用的结果主要在dictionary..Explanations字段里面。其他有些字段里面还包括音标等信息。

利用curlconverter转换为Go请求流的代码。

而后用结构体存储我们要查询的信息,得到返回的json串,json串非常多,需要反序列化得到结构体来显示结果。用oktools

用mian函数调用query函数。

代码

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

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

func query(word string) {
	client := &http.Client{}
	//var data = strings.NewReader(`{"source":"good","trans_type":"en2zh"}`)

	requset := DictRequest{TransType: "en2zh", Source: word}
	buf, err := json.Marshal(requset) //序列化
	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("Connection", "keep-alive")
	req.Header.Set("DNT", "1")
	req.Header.Set("os-version", "")
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
	req.Header.Set("app-name", "xy")
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("Accept", "application/json, text/plain, */*")
	req.Header.Set("device-id", "")
	req.Header.Set("os-type", "web")
	req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
	req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("Sec-Fetch-Site", "cross-site")
	req.Header.Set("Sec-Fetch-Mode", "cors")
	req.Header.Set("Sec-Fetch-Dest", "empty")
	req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
	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 StatusCode:", resp.StatusCode, "body", string(bodyText))
	}
	fmt.Printf("%s\n", bodyText)

	//打印查询结果
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse) //把body反序列化
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("%#v\n", dictResponse)
	//打印需要的特殊字段
	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)
}

结果展示

运行结果

SOCKS5代理

Socks5协议虽然是代理协议,但并不能用来翻墙,它的协议都是明文传输。这个协议历史比较久远,诞生于互联网早期。

Socks5相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻墙软 件,最终暴露的也是一个Socks5协议的端口。爬虫在爬取过程中很容易会遇到IP访问频率超过限制。这个时候很多人就会去网上找一些代理IP池,这些代理IP池里面的很多代理的协议就是socks5。

原理

第一步

在go里面写一个简单的TCP echo server。

  1. 在main函数里面先用net.listen去监听一个端口,会返回一个server,然后在一个死循环里面,每次去accept一个请求,成功就会返回一个连接。

  2. 在一个process函数里面去处理这个连接。 注意这前面会有个go关键字,这个代表启动一个goroutinue,可以暂时类比为其他语言里面的启动一个子线程。只是这里的goroutinue的开销会比子线程要小很多,可以很轻松地处理上万的并发。

  3. process函数的实现。

    • 加一个defer connection.close0,defer是Golang里面的一个语法,这一行的含义就是代表在这个函数退出的时候要把这个连接关掉,否则会有资源的泄露。
    • 用bufio.NewReader来创建一个带缓冲的只读流,这个在前面的猜谜游戏里面也有用到,带缓冲的流的作用是,可以减少底层系统调用的次数,比如这里为了方便是一个字节一个字节的读取,但是底层可能合并成几次大的读取操作。并且带缓冲的流会有更多的一些工具函数用来读取数据。调用那个readbyte函数来读取单个字节。再把这一个字节写进去连接。

第二步——认证阶段

浏览器会给代理服务器发送一个包,然后这个包有三个字段

  1. version也就是协议版本号,固定是5
  2. methods,认证的方法数目
  3. 每个method的编码,0代表不需要认证,2代表用户名密码认证

先用read bytes来把版本号读出来,然后如果版本号不是socket5的话直接返回报错,接下来再读取method size,也是一个字节。然后去make一个相应长度的一个slice,用io.ReadFull把它去填充进去。

第三步——请求阶段

读取到携带URL或者P地址+端口的包,然后把它打印出来。

实现一个和auth函数类似的connect函数,同样在process里面去调用。

定义一个长度为4的buffer然后把它读满。然后第0个、第1个、第3个、分别是version cmd和type,version需要判断是socket5,cmd需要判断是1。

下面的a type,可能是ipv4,ipv6,或者是host。

如果IPV4的话,再次读满这个buffer,因为这个buffer长度刚好也是4个字节,然后逐个字节打印成P地址的格式保存到addr变量。 如果是host的话,需要先读它的长度,再make一个相应长度的buf填充它。再转换成字符串保存到addr变 量。

PV6用得比较少,暂时先不支持。

最后还有两个字节那个是pot,读取它,然后按协议规定的大端字节序转换成数字。由于上面的buffer已经不会被其他变量使用了,我们可以直接复用之前的内存,建立一个临时的slice,长度是2用于读取,这样的话最多会只读两个字节回来。

接下来把这个地址和端口打印出来用于调试。收到浏览器的这个请求包之后,需要返回一个包,这个包有很多字段,但其实大部分都不会使用。

  • 第一个是版本号还是s0ckt5。
  • 第二个是返回的类型,这里是成功就返回0。
  • 第三个是保留字段填0
  • 第四个atype地址类型填1
  • 第五个,第六个暂时用不到,都填成0。 一共4+4+2个字节,后面6个字节都是0填充。

直接用net.dial建立一个TCP连接,建立完连接之后,我们同样要加一个defer来关闭连接。

接下来需要建立浏览器和下游服务器的双向数据转发。标准库的io.copy可以实现一个单向数据转发,双向转发需要启动两个goroutinue。

connect函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向copy出错的时候,再返回connect函数。可以使用到标准库里面的一个context机制,用context连with cancel来创建一个context。

最后等待ctx.Done0,只要cancel被调用,ctx.Done就会立刻返回。然后在上面的两个goroutinue里面调用一次cancel即可。

package main

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

func main() {
	// 启动一个 TCP 监听服务器,绑定在 127.0.0.1:1080
	// Listen 返回一个 Listener 对象,用于接受客户端连接
	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) // 如果连接失败,打印错误
		}
		// 为每个客户端启动一个新的 goroutine 进行处理
		go process(client)
		// 使用 goroutine 来处理客户端连接,可以轻松处理上万的并发请求
		// 由于 goroutine 的开销小于传统的线程,因此非常适合处理高并发场景
	}
}

// process 函数处理与客户端的交互,包括认证和连接请求
func process(conn net.Conn) {
	defer conn.Close()              // 确保在函数退出时关闭连接,避免资源泄露
	reader := bufio.NewReader(conn) // 创建一个 reader 用于从连接中读取数据

	// 调用认证函数
	err := auth(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed %v", conn.RemoteAddr(), err)
		return // 如果认证失败,退出处理
	}
	log.Println("auth access") // 认证通过,打印日志

	// 调用连接处理函数
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v connect failed: %v", conn.RemoteAddr(), err)
		return // 如果连接处理失败,退出处理
	}
}

// socks5 协议相关常量定义
const socks5Ver = 0x05 // SOCKS5 协议版本号
const cmdBind = 0x01   // CONNECT 命令
const atypeIPV4 = 0x01 // IPv4 地址类型
const atypeHOST = 0x03 // 域名地址类型
const atypeIPV6 = 0x04 // IPv6 地址类型

// auth 函数处理 SOCKS5 认证逻辑
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	// 认证数据格式:
	// |VER | NMETHODS | METHODS  |
	// +----+----------+----------+
	// | 1  |    1     | 1 to 255 |
	// +----+----------+----------+
	// VER: 协议版本,socks5为0x05
	// NMETHODS: 支持认证的方法数量
	// METHODS: 对应NMETHODS,支持的认证方法列表

	ver, err := reader.ReadByte() // 读取协议版本号
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err) // 如果读取失败,返回错误
	}

	if ver != socks5Ver { // 如果版本不是 SOCKS5(0x05),返回错误
		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)
	}
	log.Println("ver", ver, "method", method) // 打印版本号和认证方法

	// 向客户端发送响应,表示选择了不需要认证的方法(0x00)
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil { // 如果发送失败,返回错误
		return fmt.Errorf("write failed:%w", err)
	}

	return nil // 认证成功,返回nil
}

// connect 函数处理 SOCKS5 协议中的 CONNECT 请求
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
	// CONNECT 请求格式:
	// +----+-----+-------+------+----------+----------+
	// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER: 版本号,socks5的值为0x05
	// CMD: 0x01表示CONNECT请求
	// RSV: 保留字段,值为0x00
	// ATYP: 目标地址类型(0x01表示IPv4,0x03表示域名)
	// DST.ADDR: 目标地址,长度根据ATYP不同而不同
	// DST.PORT: 目标端口,2字节

	buf := make([]byte, 4)            // 创建一个4字节的缓冲区,用于存储请求头(VER, CMD, RSV, ATYP)
	_, err = io.ReadFull(reader, buf) // 读取请求头
	if err != nil {
		return fmt.Errorf("reader 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 := "" // 用于存储目标地址

	// 根据 ATYP 字段解析不同类型的目标地址
	switch atyp {
	case atypeIPV4: // IPv4 地址
		_, err = io.ReadFull(reader, buf) // 读取 4 字节的 IPv4 地址
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		// 将 4 字节数据格式化为 IPv4 地址
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST: // 域名地址
		hostSize, err := reader.ReadByte() // 读取域名的长度(1 字节)
		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: // 暂不支持 IPv6 地址
		return errors.New("IPv6: not supported yet")
	default: // 如果 ATYP 字段无效,返回错误
		return errors.New("invalid atyp")
	}

	// 读取目标端口(2 字节)
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed:%w", err) // 如果读取失败,返回错误
	}
	port := binary.BigEndian.Uint16(buf[:2]) // 将端口转换为大端字节序的 uint16

	// 连接目标地址和端口
	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)

	// 向客户端发送连接成功的响应
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err != nil {
		return fmt.Errorf("write failed:%w", err) // 如果发送失败,返回错误
	}

	// 使用 context 和 goroutine 进行数据转发
	// 创建一个带取消的上下文(ctx)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保在函数退出时取消上下文

	// 使用 goroutine 在客户端与目标之间转发数据
	go func() {
		_, _ = io.Copy(dest, reader) // 从客户端读取数据并写入到目标
		cancel()                     // 数据转发完成后取消上下文
	}()
	go func() {
		_, _ = io.Copy(conn, dest) // 从目标读取数据并写入到客户端
		cancel()                   // 数据转发完成后取消上下文
	}()

	// 等待上下文取消,表示数据转发完成
	<-ctx.Done()
	return nil // 成功处理请求,返回 nil
}

测试:curl --socks5 127.0.0.1:1080 -v http://www.qq.com

结果如图

结果图