Go 语言的实战案例 | 青训营笔记

115 阅读4分钟

Go 语言的实战案例

猜谜游戏

版本1

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	maxNum := 100
	secretNumber := rand.Intn(maxNum) //rand.Intn() 函数返回 0-N 之间的随机数。这里,N 是函数 rand.Intn() 中指定的数字。
	fmt.Println("The secret number is ", secretNumber)
}

rand.Intn() 函数返回 0-N 之间的随机数。这里,N 是函数 rand.Intn() 中指定的数字。

版本2

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	maxNum := 100
	rand.Seed(time.Now().UnixNano()) //设置随机数种子
	secreatNum := rand.Intn(maxNum)
	fmt.Println("The secreat number is ", secreatNum)
}

rand设置随机种子是全局性的。如果不初始化种子的默认随机源,用 rand 库是协程安全的。

在多协程情况下,应该重新新建个rand,参考grpc-go的做法:

package grpcrand

import (
	"math/rand"
	"sync"
	"time"
)

var (
	r  = rand.New(rand.NewSource(time.Now().UnixNano()))
	mu sync.Mutex
)

// Int63n implements rand.Int63n on the grpcrand global source.
func Int63n(n int64) int64 {
	mu.Lock()
	res := r.Int63n(n)
	mu.Unlock()
	return res
}

// Intn implements rand.Intn on the grpcrand global source.
func Intn(n int) int {
	mu.Lock()
	res := r.Intn(n)
	mu.Unlock()
	return res
}

// Float64 implements rand.Float64 on the grpcrand global source.
func Float64() float64 {
	mu.Lock()
	res := r.Float64()
	mu.Unlock()
	return res
}

版本3

package main

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

func main() {
	//maxNum := 100
	rand.Seed(time.Now().UnixNano())
	//secreatNum := rand.Intn(maxNum)
	//读取io
	fmt.Println("Please input your guess")
	reader := bufio.NewReader(os.Stdin)
	input, err := reader.ReadString('\n')
	if err != nil {
		fmt.Println("An error occured while reading input. Please try again", err)
		return
	}
	//windows的换行是\r\n,unix的是\n
	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)

}

这里需要注意的地方就是go语言不允许未使用的变量,这跟其它语言有点区别。

在windows平台上,换行符是\r\n,而linux是\n,视频使用\n,应该代码是在linux上运行。

版本4

package main

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

// 与大多数程序语言不同,Go不允许未使用的变量。这是因为设计决策在此级别上强制执行,而不是像其他语言那样作为每个开发人员的可选选择。
// 因此,您可以使用空白标识符作为提到的另一个答案,但这是因为Go的工作方式
func main() {
	maxNum := 100
	rand.Seed(time.Now().UnixNano())
	secreatNum := rand.Intn(maxNum)
	fmt.Println("Please input your guess")
	for {
		//读取io

		reader := bufio.NewReader(os.Stdin)
		input, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("An error occured while reading input. Please try again", err)
			continue
		}
		//windows的换行是\r\n,unix的是\n
		input = strings.TrimSuffix(input, "\r\n")
		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter an integer value")
			continue
		}

		if guess > secreatNum {
			fmt.Println("Your guess is bigger than the secret number. Please try again")
		} else if guess < secreatNum {
			fmt.Println("Your guess is smaller than the secret number. Please try again")
		} else {
			fmt.Println("Correct,you Legend!")
			break
		}
	}

}

爬虫实战

package main

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

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
}
type DictResponse struct {
	Rc         int        `json:"rc"`
	Wiki       Wiki       `json:"wiki"`
	Dictionary Dictionary `json:"dictionary"`
}
type Wiki struct {
}
type Prons struct {
	EnUs string `json:"en-us"`
	En   string `json:"en"`
}
type Dictionary struct {
	Prons        Prons         `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"`
}

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage:simpleDict Word example:simpleDict hello`) //这里字符串不是用'而是要用`
		os.Exit(1)
	}
	word := os.Args[1]
	client := &http.Client{}
	//var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
	request := DictRequest{TransType: "en2zh", Source: word}
	buf, err := json.Marshal(request)
	if err != nil {
		log.Fatal(err)
	}
	var data = bytes.NewBuffer(buf)
	req, err := http.NewRequest("POST", "<https://api.interpreter.caiyunai.com/v1/dict>", data)
	if err != nil {
		log.Fatal(err)
	}
	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,en-GB;q=0.7,en-US;q=0.6")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("device-id", "687688507465ec9586040967ca42e24b")
	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=8, Chromium;v=114, Microsoft")
	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("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.0.0")
	req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
	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)
	}
	// fmt.Printf("%s\n", bodyText)
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	// fmt.Printf("%#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)
	}
}

注意点: 这里字符串不是用'而是要用`

fmt.Fprintf(os.Stderr,  `usage:simpleDict Word example:simpleDict hello`)

浏览器复制出来的curl命令需要将^替换成\,才可以使用curl command to Go工具进行转换 image.png

Socks5代理

package main

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

func main() {
	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)
			continue
		}
		go process(client)
	}
}

//	func process(conn net.Conn) {
//		defer conn.Close()
//		reader := bufio.NewReader(conn)
//		for {
//			b, err := reader.ReadByte()
//			if err != nil {
//				break
//			}
//			_, err = conn.Write([]byte{b})
//			if err != nil {
//				break
//			}
//		}
//	}
// func process(conn net.Conn) {
// 	defer conn.Close()
// 	reader := bufio.NewReader(conn)
// 	err := auth(reader, conn)
// 	if err != nil {
// 		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
// 		return
// 	}
// 	log.Println("auth success")
// }

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	err := auth(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
	// log.Println("auth success")
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v connect failed:%v", conn.RemoteAddr(), err)
		return
	}
}

const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeTPV6 = 0x04

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)
	}
	mehodSize, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read methodSize failed:%w", err)
	}
	method := make([]byte, mehodSize)
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
	log.Println("ver", ver, "method", method)

	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", 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 header failed:%w", err)
	}
	ver, cmd, atyp := buf[0], buf[1], buf[3]
	if ver != socks5Ver {
		return fmt.Errorf("read header failed:%w", err)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}
	addr := ""
	switch atyp {
	case atypIPV4:
		_, 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 atypeTPV6:
		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))
	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
}

![图片转存失败,建议将图片保存下来直接上传

socks5连接过程:

首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段

第一个握手阶段,浏览器会向 socks5 代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。

第三个阶段是请求阶段,认证通过之后浏览器会 socks5 服务器发起请求。主要信息包括 版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。

第四个阶段是 relay 阶段。此时浏览器会发送 正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上 代理服务器并不关心流量的细节,可以是 HTTP流量,也可以是其它 TCP 流量。 这个就是 socks5 协议的工作原理

image.png 协商认证报文格式(以字节为单位)::

VERNMETHODSMETHODS
111到255

VER是SOCKS版本,这里应该是0x05;

NMETHODS是METHODS部分的长度;

METHODS是客户端支持的认证方式列表,每个方法占1字节。当前的定义是:

  1. 0x00 不需要认证
  2. 0x01 GSSAPI
  3. 0x02 用户名、密码认证
  4. 0x03 – 0x7F由IANA分配(保留)
  5. 0x80 – 0xFE为私人方法保留
  6. 0xFF 无可接受的方法

浏览器的请求报文格式:

VERCMDRSVATYPDST.ADDRDST.PORT
11X'00’1Variable2

CMD是SOCK的命令码:

  1. 0x01表示CONNECT请求
  2. 0x02表示BIND请求
  3. 0x03表示UDP转发

RSV 默认 0x00,保留

ATYP 后面的地址类型:

  1. 0x01 IPv4地址,DST ADDR部分4字节长度
  2. 0x03 域名,DST ADDR部分第一个字节为域名长度,DST ADDR剩余的内容为域名,没有\0结尾。
  3. 0x04 IPv6地址,16个字节长度。

DST.ADDR 目的地址

DST.PROT  目的端口

注意:如果为UDP 这里的目的地址与端口为 客户端的UDP信息。告诉代理服务器以后会以这个地址进行UDP信息交互。

服务器对请求报文进行确认的格式:

VERREPRSVATYPBND.ADDRBND.PORT
11X'00’1Variable2

REP 应答字段

  1. 0x00 表示成功
  2. 0x01 普通SOCKS服务器连接失败
  3. 0x02 现有规则不允许连接
  4. 0x03 网络不可达
  5. 0x04 主机不可达
  6. 0x05 连接被拒
  7. 0x06 TTL超时
  8. 0x07 不支持的命令
  9. 0x08 不支持的地址类型
  10. 0x09 0xFF未定义

BND.ADDR 服务器绑定的地址

BND.PORT 以网络字节顺序表示的服务器绑定的端口

上述流程完成后,将会进行数据转发的过程。

	go func() {
		_, _ = io.Copy(dest, reader)
		cancel()
	}()
	go func() {
		_, _ = io.Copy(conn, dest)
		cancel()
	}()

注意:

使用curl发送请求命令如下,这里添加了参数-4,意思是用ipv4地址:

curl --socks5 127.0.0.1:1080 -4 -v <http://www.qq.com>