字节青训营笔记 day1

118 阅读7分钟

golang 使用

  1. 官网下载
  2. ide 下载(golang 或 vscode)
  3. 环境测试

golang 语法入门

变量和常量

package main

import (
  "fmt"
  "math"
)

func main() {
  // 字符串变量声明 (短变量声明更加简洁)
  var a = "initial"
  b := "init"

  // 整型变量声明(可同时声明多个同类型变量)
  var c int 
  var c1, c2 int

  // 布尔类型变量声明
  var d = true  

  // 浮点类型变量声明 (float32、float64)
  var e float64 
  
  // golang 中进行类型转换,需要显式声明
  f := float32(e)

  // 字符串类型变量拼接, 可直接使用 '+' 进行拼接
  g := a + "foo"
  
  // 在 golang 中,声明后的变量都必须使用,否则会报错
  fmt.Println(a, b, c, c1, c2, d, e, f)
  fmt.Println(g)

  // 如果暂时用不到某个变量,又不想编译错误,那么可以使用以下方式进行处理 _ 表示省略
  // _ = f

  // 常量
  const s string = "constant"
  const h = 50000
  // 科学计数法
  const i = 3e20 / h
  // 数学相关包 math
  fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}

时间处理

// 当前时间
now := time.Now()
fmt.Println(now) // 2022-03-27 18:04:59.433275 +0800 CST m=+0.000087933

// 自定义设置时间 
t := time.Date(2022, 3, 27, 1, 25, 26, 0, time.UTC) 
t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC) 
fmt.Println(t) // // 2022-03-27 01:25:36 +0000 UTC
fmt.Println(t.Year(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05"))  // 2022-03-27 13:25:36
fmt.Println(t.Format("2006-01-02 03:04:05")) // 2022-03-27 01:25:36

// 求两个时间的差值
diff := t2.Sub(t)
fmt.Println(diff) // 1h5m0s
// 差值表示形式默认为 hour 也可以转换成分钟和秒来表示
fmt.Println(diff.Minutes(), diff.Second()) // 65 3900

// 将 2022-03-27 01:25:36 转换为指定格式
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36") 
if err != nil {
    panic(err)
}

fmt.Println(t3 == t)  
fmt.Println(now.Unix())  // 时间戳

数字解析

// 将字符串 "1.234" 转换为 64 位浮点数
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234

// 将 "111" 转换为 10 进制 64 位整数
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111

// 自动推断 "0x1000" 的类型,并转换为 64 位整数值
n, _ = strconv.ParseInt("0x1000", 0, 64) 
fmt.Println(n2) // 4096

// 将字符串转换为整型
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123

n2, err := strconv.Atoi("aaa")
if err != nil {
    fmt.Println(err) // parsing "aaa": invalid syntax
    return
}

fmt.Println(n2) 

进程信息

    // go run example/20-env/main.go a b c d 打印命令行参数
    fmt.Println(os.Args) // [/var/folders/8p/n.../exe/main a b c d]
    // 获取环境变量值
    fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin
    // 写入环境变量值
    fmt.Println(os.Setenv("AA", "BB"))
    
    // 快速启动子进程并获取输入输出
    buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
    
    if err != nil {
        panic(err)
    }
    
    fmt.Println(string(buf))

golang 实战

实战 1----猜谜游戏

单次猜谜逻辑

func main() {
    // 猜谜游戏最大数字
    maxNum := 100
    // 随机数种子,否则使用 rand 函数得到都是同一个数字
    rand.Seed(time.Now().UnixNano()) 
    // 获取一个随机目标数
    secretNumber := rand.Intn(maxNum)
    fmt.Println("The secret number is ", secretNumber)
    fmt.Println("Please input your guess")
    
    // 将输入的内容加载到只读流中,方便格式处理
    reader := bufio.NewReader(os.Stdin)
    // 使用 ReadString 方法读取只读流中的字符串数据
    input, err := reader.ReadString('\n')
    // 错误处理
    if err != nil {
        fmt.Println("An error occured while reading input. Please try again", err)
        return 
    }
    // 去掉末尾的换行符
    input = strings.TrimSuffix(input, '\n')
    
    guess, err := strconv.Atoi(input)
    if err != nil {
        fmt.Println("Invalid input. Please enter an integer value")
        return
    }
    
    fmt.Println("Your guess is ", guess)
    
    if guess != secretNumber {
        fmt.Println("you guess is wrong")
    } else {
        fmt.Println("you guess is true")
    }
}

循环猜谜逻辑

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("Please input your guess")
	reader := bufio.NewReader(os.Stdin)
	for {
		input, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("An error occured while reading input. Please try again", err)
			continue
		}

		input = strings.TrimSuffix(input, "\n")

		guess, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input. Please enter an interger value")
			continue
		}

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

image.png

实战 2----在线词典

抓包

image.png

image.png

将 cURL 请求转换为 golang 语言格式

image.png

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
)

func main() {
        // 创建 http client,创建的同时也可以指定很多参数(比如最重要的 timeout)
	client := &http.Client{}
        // 正常情况下我们输入的是字符串,这里使用 strings.NewReader函数将其转换为只读流
        // 防止超大文件放在内存中读取失败。只需要占用很小的内存
	var data = strings.NewReader(`{"source":["app","",""],"trans_type":"auto2zh","request_id":"web_fanyi","media":"text","os_type":"web","dict":true,"cached":true,"replaced":true,"detect":true,"browser_id":"55146e426fa00cfe0d8cabae0402e69d"}`)
        // 创建请求 http.Request(Method, Url, data)
	req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/translator", 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,zh-TW;q=0.7")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("device-id", "")
	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", "Google Chrome";v="109", "Chromium";v="109"`)
	req.Header.Set("sec-ch-ua-mobile", "?0")
	req.Header.Set("sec-ch-ua-platform", `"macOS"`)
	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("t-authorization", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJicm93c2VyX2lkIjoiNTUxNDZlNDI2ZmEwMGNmZTBkOGNhYmFlMDQwMmU2OWQiLCJpcF9hZGRyZXNzIjoiMjIwLjExNS4xNTkuNiIsInRva2VuIjoicWdlbXY0anIxeTM4anlxNnZodmkiLCJ2ZXJzaW9uIjoxLCJleHAiOjE2NzUyMzc4Nzh9.C3fBG6TaVJcmVGPBGH1kA17czlvZmOUMGu8oRc7YluY")
	req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
	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)
}

生成 request body

type DictRequest struct {
    TransType string `json:"trans_type"`
    Source string `json:"source"`
    UserID string `json:"user_id"`
}

func main() {
    client := &http.Client{}
    request := DictRequest{"TransType": "en2zh", "Source": "good"}
    // 构造请求结构体并使用 json 进行序列化为字节数组形式
    buf, err := json.Marshal(request)
    if err != nil {
        log.Fatal(err)
    }
    
    // 从 buf 中读取数据到只读流中
    var data = bytes.NewReader(buf)
    // http.NewRequest(Method, Url, data)
    req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
    if err != nil {
        log.Fatal(err)
    }
}

解析 response body

image.png api 返回的结构非常复杂,使用 json 转 golang struct进行转换

bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
    log.Fatal(err)
}

// DictResponse 结构体由 preview 中的 json 字段转换而来
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
    log.Fatal(err)
}
// 详细结果
fmt.Prtinf("%#v\n", dictResponse)

打印结果

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

// 将 resp 反序列化到结构体中
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 query(word string) {
    client := &http.Client{}
    
    //request := DictRequest{TransType: "en2zh", Source: "good"}
    request := DictRequest{TransType: "en2zh", Source: word}
    buf, err := json.Marshal(request)
    ...
}

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

最终代码

代码地址

实战 3----socks5 代理

socks5 代理 ---- 原理

image.png

正常浏览器访问一个网站,如果不经过代理,需要先和对方网站服务器建立 TCP 连接(3 次握手,在握手完成之后发起 HTTP 请求,服务器返回 HTTP 响应)。

如果经过代理服务器的话,流程相对比较复杂,首先浏览器要和 socks5 代理服务器建立 TCP 连接,代理服务器再和真正的服务器建立 TCP 连接。过程可以分为 4 个阶段

  1. 协商阶段:用户的浏览器会向 socks5 代理服务器发送请求报文,报文中包含协议版本号、支持的认证种类等,socks5 代理服务器会从支持的认证类型中选择一个自己也支持的认证方式返回给浏览器。
  2. 认证阶段
  3. 请求阶段:认证通过后,用户浏览器会向socks5代理服务器发送下一个请求报文,包括协议版本号、请求的类型(一般都是 connection 请求,请求代理服务器要和指定域名(某个域名及某个端口)建立 TCP 连接),代理服务器收到来自用户浏览器的请求后,就会和指定后端服务器建立 TCP 连接,然后返回响应报文告诉用户浏览器:我已经成功建立与指定域名的 TCP 连接了。
  4. relay阶段:浏览器正常发送请求,socks5 代理服务器简单的将请求转发给真正的后端服务器,如果真正的后端服务器返回的有响应的话,socks5 代理服务器把响应结果转发给浏览器。

代理服务器并不关心流量的细节:可以是 HTTP 流量,也可以是 TCP 流量。

socks5 代理 ---- TCP echo server

func main() {
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    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
        }
    }
}

测试:

  1. 打开终端 1,编译并运行程序,保持终端挂起
  2. 再打开一个终端 2,使用 nc 命令(直接和某个 ip:port 建立 TCP 连接,即和终端 1)
  3. 在终端 2 输入 hello,查看结果

image.png

socks5 代理 ---- auth

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

	// 读取第一个字节 VER
	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed: %v", err)
	}

	if ver != socks5Ver {
		return fmt.Errorf("not supported ver: %v", ver)
	}

	// 读取第二个字节 NMETHODS
	methodSize, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read methodSize failed %v", err)
	}

	method := make([]byte, methodSize)
        // 使用 io.ReadFull 将 method 填充满
	_, err = io.ReadFull(reader, method)

	if err != nil {
		return fmt.Errorf("read method failed: %v", err)
	}

	log.Println("ver", ver, "method", method)

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

	return nil
}

socks5 代理 ---- 请求阶段

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)
	// 将 buf 填充满(4 个字节)
	_, err = io.ReadFull(reader, buf)

	if err != nil {
		return fmt.Errorf("read header failed: %v", err)
	}
	// buf [ver cmd rsv atyp]
	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 atypIPV4:
		// 如果是 IPV4 类型, 那么从只读流中读取 buf 字节(域名)
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atyp failed: %v", err)
		}

		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		// 如果是 HOST 类型, 紧接着的一个字节是 HOST 的大小
		hostSize, err := reader.ReadByte()
		if err != nil {
			return fmt.Errorf("read hostSize failed: %v", err)
		}

		host := make([]byte, hostSize)
		// 填满 host
		_, err = io.ReadFull(reader, host)
		if err != nil {
			return fmt.Errorf("read hostSize failed: %v", err)
		}
		// 将字节形式的 host 转换为 string 类型即可
		addr = string(host)
	case atypeIPV6:
		return errors.New("IPV6: not suppoted yet")
	default:
		return errors.New("invalid atyp")
	}
	// port 2 bytes
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed: %v", err)
	}

	port := binary.BigEndian.Uint16(buf[:2])

	log.Println("dial", addr, port)

	// 给浏览器回复
	// +------+------+------+------+-----------+----------+
	// | VER  |  REP |  RSV | ATYP |  BND.ADDR | BND.PORT |
	// +------+------+------+------+-----------+----------+
	// |   1  |  1   | X'00'|  1   |  Variable |    2     |
	// +------+------+------+------+-----------+----------+
	// VER socks 版本
	// REP: Relay field 内容取值如下: X'00' succeed
	// 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: %v", err)
	}

	return nil
}

socks5 代理 ---- relay 阶段

    port := binary.BigEndian.Uint16(buf[:2])
    // socks 代理服务器建立与ADDR:PORT的 TCP 连接
    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) 

io.Copy :可实现单向数据转发, 使用代理服务器,需要两个方向的数据转发,因此需要启动两个 goroutine,调用两次 io.Copy 进行数据转发操作

函数签名: func Copy(dst Writer, src Reader) (written int64, err error)

    // 按照响应格式返回响应
    _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
        return fmt.Errorf("write failed: %w", err)
    }
    // 上下文,用于控制子goroutine的退出
    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

由于启动 goroutine 几乎是不耗费时间的,如果不使用 context 控制子 goroutine 的生命周期,那么程序会直接跑到最后一行,导致函数直接返回,连接也会被断开。程序应实现:当两个方向中任一方向的 Copy 失败(有一方已经关闭连接了)时,才会终止整个连接(在此之前主 goroutine 都应该等待子 goroutine 的结束)。

使用 <-ctx.Done() 检测是否有子 goroutine 中的 cancel() 函数被调用,一旦某个子 goroutine 发生错误,cancel()函数将会被调用,此时会向通道中发送 cancel 信号,父 goroutine 从通道中收到信号后才会选择断开连接,否则通道一直处于阻塞状态(一直等待结束信号)。

socks5 代理 ---- 全部代码

代码地址

源码下载到了本地的 /Users/apple/Public/青训营/day1