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

59 阅读10分钟

猜数游戏

func main() {
	maxNum := 100
	rand.Seed(time.Now().UnixNano())
	num := rand.Intn(maxNum)

	for {
		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

		}
		input = strings.Trim(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)
		if guess > num {
			fmt.Println("Your guess is bigger than the secret number. Please try again")
		} else if guess < num {
			fmt.Println("Your guess is smaller than the secret number. Please try again")
		} else {
			fmt.Println("Correct, you Legend!")
			break
		}
	}
}
rand.Seed(time.Now().UnixNano())

在 Go 语言中,rand.Seed 方法用于初始化随机数生成器的种子。随机数生成器是一个伪随机数生成器,它使用一个种子来生成随机数序列。如果种子相同,那么生成的随机数序列也相同。

为了避免生成相同的随机数序列,通常将种子设置为一个随机值。一种常见的方法是使用当前时间的纳秒级别表示作为种子。这样,每次运行程序时,都会生成一个不同的随机数序列。

reader := bufio.NewReader(os.Stdin)

os.Stdin 是一个标准输入流,它是一个指向 *os.File 类型的指针,表示程序的标准输入。在 Go 语言中,标准输入流通常用于从控制台或者其他输入设备中读取用户输入的数据。

input, err := reader.ReadString('\n')

reader.ReadString()是一个用于读取输入流的方法。它的作用是从输入流中读取一行数据,直到遇到换行符(\n)为止,并将读取到的数据作为字符串返回。

需要注意的是,reader.ReadString('\n') 方法返回的字符串包含了输入流中的换行符。

input = strings.Trim(input, "\r\n")

strings.Trim(input, "\r\n") 方法将输入数据中的换行符去除,并将结果存储在 input 变量中。这样,input 变量中存储的就是用户的输入,而不包含任何换行符或其他空白字符。

\r 是一个转义字符,表示回车符(Carriage Return)。

在线词典

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"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 {
		KnownInLaguages int `json:"known_in_laguages"`
		Description     struct {
			Source string      `json:"source"`
			Target interface{} `json:"target"`
		} `json:"description"`
		ID   string `json:"id"`
		Item struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"item"`
		ImageURL  string `json:"image_url"`
		IsSubject string `json:"is_subject"`
		Sitelink  string `json:"sitelink"`
	} `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{}
	request := DictRequest{TransType: "en2zh", Source: word}
	buf, err := json.Marshal(request)
	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 := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	if resp.StatusCode != 200 {
		log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
	}
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	if err != nil {
		log.Fatal(err)
	}
	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)
}
client := &http.Client{}

在 Go 语言中,http.Client 是一个 HTTP 客户端,它提供了发送 HTTP 请求和接收 HTTP 响应的功能。&http.Client{} 是一个创建 http.Client 实例的语法,它使用花括号 {} 创建了一个空的结构体,并使用 & 取其地址,返回一个指向结构体的指针。

创建 http.Client 实例时,可以指定一些配置参数,例如超时时间、代理设置等。这些字段用于控制 HTTP 请求和响应的行为。

var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)

strings.NewReader 函数将一个 JSON 格式的字符串作为参数,创建了一个读取器,用于读取该字符串中的数据。该字符串表示一个包含两个字段的 JSON 对象

req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)

将该 JSON 对象作为 HTTP 请求的请求体发送给服务器,可以创建一个 http.Request 实例,并将读取器作为请求体。

resp, err := client.Do(req)

http.Client 提供了 Do 方法用于发送 HTTP 请求并返回响应结果。client.Do(req) 表示使用指定的 http.Client 客户端实例发送 HTTP 请求,并返回一个响应结果。其中,req 是一个 http.Request 实例,表示待发送的 HTTP 请求。

在发送 HTTP 请求时,client.Do(req) 方法会根据请求的 URL、方法、请求头、请求体等信息,构建一个 HTTP 请求,并将其发送到目标服务器。服务器处理完请求后,会返回一个 HTTP 响应,并将响应结果封装成一个 http.Response 实例。

resp 变量表示 HTTP 响应结果,它包含了响应的状态码、响应头、响应体等信息。在发送 HTTP 请求后,我们通常需要检查错误,例如网络连接失败、服务器返回错误的响应等情况。如果发生错误,err 变量将包含错误信息;否则,err 变量将为 nil。

bodyText, err := ioutil.ReadAll(resp.Body)

ioutil.ReadAll 函数用于读取一个 io.Reader 接口的所有数据,并将其存储为一个字节数组。resp.Body 是一个 io.ReadCloser 接口,表示 HTTP 响应的响应体。因此,ioutil.ReadAll(resp.Body) 表示读取 HTTP 响应的响应体中的所有数据,并将其存储为一个字节数组。

if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		os.Exit(1)
	}
	word := os.Args[1]

通过 os.Args 变量来获取命令行参数。os.Args 是一个字符串切片,其中第一个元素是程序的名称,后面的元素是程序接收到的命令行参数。因此,在 main.go 程序中,可以通过 os.Args[1] 来获取第二个命令行参数,即 "word",就是我们要查询的单词

运行结果

SOCKS5 代理

SOCKS5 代理 介绍

SOCKS5 代理是一种使用 SOCKS5 协议实现的网络代理,用于在客户端和目标服务器之间转发网络流量。当客户端需要发送网络请求时,它会将请求发送给 SOCKS5 代理服务器,代理服务器会将请求转发给目标服务器,并将响应结果返回给客户端。客户端和代理服务器之间的通信使用 SOCKS5 协议加密传输,可以保护数据的安全性。SOCKS5 代理通常用于访问被墙的网站、隐藏 IP 地址、加速网络传输速度等场景。

package main

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

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

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)
	err := auth(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	// +----+----------+----------+
	// |VER | NMETHODS | METHODS  |
	// +----+----------+----------+
	// | 1  |    1     | 1 to 255 |
	// +----+----------+----------+
	// VER: 协议版本,socks5为0x05
	// NMETHODS: 支持认证的方法数量
	// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
	// X’00’ NO AUTHENTICATION REQUIRED
	// X’02’ USERNAME/PASSWORD

	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)
	}

	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_, 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) {
	// +----+-----+-------+------+----------+----------+
	// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER 版本号,socks5的值为0x05
	// CMD 0x01表示CONNECT请求
	// RSV 保留字段,值为0x00
	// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
	//   0x01表示IPv4地址,DST.ADDR为4个字节
	//   0x03表示域名,DST.ADDR是一个可变长度的域名
	// DST.ADDR 一个可变长度的值
	// DST.PORT 目标端口,固定2个字节

	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))
	if err != nil {
		return fmt.Errorf("dial dst failed:%w", err)
	}
	defer dest.Close()
	log.Println("dial", addr, port)

	// +----+-----+-------+------+----------+----------+
	// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER socks版本,这里为0x05
	// REP Relay field,内容取值如下 X’00’ succeeded
	// RSV 保留字段
	// ATYPE 地址类型
	// BND.ADDR 服务绑定的地址
	// BND.PORT 服务绑定的端口DST.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
}
for {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept failed %v", err)
			continue
		}
		go process(client)
	}

一个无限循环,不断等待客户端的连接请求。在循环中,使用 server.Accept() 方法接受客户端连接,并返回一个表示客户端连接的 net.Conn 实例。

当程序接受到客户端连接请求后,会调用 process 函数来处理客户端发送的数据。由于 process 函数是在新的 goroutine 中运行的,因此程序可以同时处理多个客户端连接。

如果客户端没有发送连接请求,server.Accept() 方法会一直阻塞等待,直到有客户端发送连接请求为止。

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

使用 reader.ReadByte() 方法从客户端读取一个字节的数据,并将其存储到变量 b 中。如果读取数据失败,则跳出循环。否则,使用 conn.Write([]byte{b}) 方法将读取到的数据原样返回给客户端。如果写入数据失败,则跳出循环。

func connect(reader *bufio.Reader, conn net.Conn) (err error)

该代码实现了 Socks5 代理服务器的处理逻辑。当客户端发送一个连接请求时,代理服务器会执行以下操作:

  1. 读取客户端发送的请求头部,并解析出请求的版本号、命令类型、地址类型等参数;
  2. 根据请求的地址类型和地址信息建立连接,并将连接返回的地址和端口号发送给客户端,表示连接已经建立;
  3. 开启两个协程,分别将客户端和目标服务器的数据流进行转发,实现数据的传输。

在代码实现中,首先读取了请求头部的前4个字节,解析出版本号、命令类型和地址类型。然后根据地址类型的不同,读取后续的地址信息,最终建立连接。接着,代理服务器向客户端发送响应头部,表示连接建立成功。最后,代理服务器开启两个协程,分别将客户端和目标服务器的数据流进行转发,实现数据的传输。

需要注意的是,该代码中仅实现了对 IPv4 地址和域名地址的支持,对 IPv6 地址的支持尚未实现。

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

<-ctx.Done()

这段代码使用了 Go 语言标准库中的 context 包,实现了对协程的取消操作。

首先,使用 context.Background() 函数创建了一个空的上下文(context),并使用 context.WithCancel() 函数创建了一个带有取消函数的上下文。取消函数可以用来取消上下文的相关操作,从而取消正在进行的协程。

然后,代码开启了两个协程,分别将目标服务器的响应数据流和客户端的请求数据流进行转发。在这两个协程中,使用 io.Copy() 函数进行数据的传输。同时,协程的运行是与上下文相关联的,即当上下文被取消时,协程也会被取消。

最后,使用 <-ctx.Done() 接收上下文的完成通知。当上下文被取消时,上下文的 Done() 方法会返回一个已关闭的通道,从而可以通过该通道接收到取消的信号。接收到该信号后,函数即可返回,实现了协程的取消操作。

开启代理