02 Go语言实战案例 | 青训营

111 阅读5分钟

第一个项目:猜数字游戏

P1 生成随机数

在1.20版本的Go中,由于已经弃用了rand.Seed函数,因此已经不需要再使用时间戳来初始化随机数种子。可以直接通过下列代码来实现指定范围的随机数生成:

func main(){
	maxNum:=100
	secretNumber := rand.Intn(maxNum)  
	fmt.Println("The secret number is ", secretNumber)
}

P2 读取用户输入

主要分为三个部分:

  • 读取用户输入
  • 去除换行符
  • 转换输入类型

在处理用户输入数据的时候,需要考虑到不同平台的输入。在Windows系统中,用户输入的每一行末尾通常由两个字符构成:\r (回车符)和 \n (换行符)。这被称为回车换行(CRLF)标记。而在其他操作系统(如Linux和macOS)上,通常只有\n字符表示一行的结束,称为换行(LF)标记。所以,当用户在Windows系统下输入一行文字,按下Enter键后,实际输入的内容是hello\r\n。而在其他系统下输入的内容是hello\n

最终实现的代码如下:

fmt.Println("Please input your guess")  
reader := bufio.NewReader(os.Stdin)  
input, err := reader.ReadString('\n') //这里读取一行输入  
if err != nil {  
	fmt.Println("An error occurred while reading input. Please try again", err)  
	return  
}  
input = strings.TrimSuffix(input, "\r\n") //这里去除换行符  
  
guess, err := strconv.Atoi(input) //将输入转换为数字  
if err != nil {  
	fmt.Println("Invalid input. Please enter an integer value")  
	return  
}  
fmt.Println("You guess is ", guess)

P3 实现判断逻辑

采用常规的if else语句即可实现。

P4 实现游戏循环

将总语句放入for循环,屏蔽掉随机数输出,再将判断语句中的return全部改为continue,再在最终判断胜利的时候再进行break即可。

最终实现

源代码如下:

package main  
  
import (  
	"bufio"  
	"fmt"  
	"math/rand"  
	"os"  
	"strconv"  
	"strings"  
)  
  
func main() {  
	/*生成随机数*/  
	maxNum := 100  
	//rand.Seed(time.Now().UnixNano())语句中,Seed已被弃用  
	secretNumber := rand.Intn(maxNum)  
	//fmt.Println("The secret number is ", secretNumber),这里屏蔽输出  
  
	/*读取用户输入并解析*/  
	fmt.Println("Please input your guess")  
	reader := bufio.NewReader(os.Stdin)  
	for {  
		input, err := reader.ReadString('\n') //这里读取一行输入  
		if err != nil {  
			fmt.Println("An error occurred while reading input. Please try again", err)  
			continue  
		}  
		input = strings.TrimSuffix(input, "\r\n") //这里去除换行符  
  
		guess, err := strconv.Atoi(input) //将输入转换为数字  
		if err != nil {  
			fmt.Println("Invalid input. Please enter an integer value")  
			continue  
		}  
		fmt.Println("You guess is ", guess)  
  
		/*实现判断逻辑*/  
		if guess > secretNumber {  
			fmt.Println("Your guess is bigger than the secret number. Please try it again")  
		} else if guess < secretNumber {  
			fmt.Println("Your guess is smaller than the secret number. Please try it again")  
		} else {  
			fmt.Println("Correct.")  
			break  
		}  
	}  
}

第二个项目:在线词典

P1 抓包

本次选用了彩云小译作为词典工具。

抓取方式:

  1. 打开翻译页面,F12开发者工具
  2. 打开网络页面,在翻译框随意输入一个单词,等待返回
  3. 找到dict项,确定有正确的翻译结果

P2 代码生成

利用curlconverter工具实现请求代码生成。

步骤:

  1. 找到dict项后,右键选择“复制-复制为cURL(bash)”
  2. 打开工具,选择Go语言,将代码粘贴到输入框中
  3. 复制输出结果

P3 生成request body

通过构建结构体来实现。

P4 解析response body

利用JSON转Golang Struct工具实现。复制 P1 中抓取到的dict项的预览json到工具中,根据需要选择相应的转换,再粘贴到总代码中即可。

但是,解析后输出的结果有许多是不需要的,因此我们需要控制相应的输出。

P5 打印结果

从P4中输出的json代码中提取出所需要的音标信息和翻译信息并输出即可。

以下是实现片段:

	fmt.Println(word, " UK: ", dictResponse.Dictionary.Prons.En, " US: ", dictResponse.Dictionary.Prons.EnUs)  
	for _, item := range dictResponse.Dictionary.Explanations {  
		fmt.Println(item)
	}

P6 完善代码

将整个请求及返回打包成query函数,再在main函数中调用即可。

最终实现

参考官方GitHub项目。

第三个项目:SOCKS5代理

原理

原理图如下图所示。实际建立的是TCP连接。

image.png

P0 TCP echo server

建立TCP echo server,可以实现输入什么就输出什么,可以用于测试server的正确性。

这里有个大坑:Windows环境下原生不支持视频中用于测试的nc命令,安装替代用的ncat后会被杀毒软件直接拦截无法运行。还请考虑保证当前网络环境安全的情况下再测试。

P1 auth(认证阶段)

完整的auth函数如下所示:

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err)
	}
	if ver != socks5Ver {
		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)
	}

	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

P2 请求阶段

在本函数中,我们需要完整的读出 ver, cmd, rsv, atyp, dst.addr, dst.port这六个完整的字符串。完整的请求阶段代码如下:

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 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 := ""
	switch atyp {
	case atypeIPV4:
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, err := reader.ReadByte()
		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:
		return errors.New("IPv6: no supported yet")
	default:
		return errors.New("invalid atyp")
	}
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(buf[:2])

	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))//这里实现relay阶段的功能
	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)
	}
	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
}

P3 relay 阶段

实现与真正服务器的tcp连接。

最终测试

代码编写完成后,在Chrome浏览器中需要安装一个SwitchyOmega插件来配置代理。