Go语言基础 | 青训营笔记

95 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

首先是课堂的内容

Go快速上手

变量

通过var声明变量,类型与其他语言类似,如string、int、float等。如果不指定类型,go语言可以推断类型

同时也能指定const类型

if-else

if-else语句没有括号(),但是必须加大括号{}

if num := 9; num < 0 {
    //
}else if num < 10 {
    //
}else {
    //
}

循环

只有for循环,for循环中的三项可以任意忽略。

同样和if-else一样不需要小括号

for i := 7; i < 10; i ++ {
    fmt.Println(i)
}
for {
    if xxx {
        break;
    }
}

switch

switch后跟变量名,case后跟变量的情况。case后面可以不跟大括号

a := 2;
switch a{
case 1 : //
case 2 :
}

数组

很少使用,定义的方法:

var a [5]int
var b [5][5]int;
var c := [5]int{1, 2, 3, 4, 5}

切片

切片相当于vector,是不定长的数组,使用make创建。可以像数组一样去取值,也可以用append追加元素,但是必须赋值给原切片

s := make([]string, 3)
s = append(s, "d")
var c := make([]string, len(s))

map

同其他语言一样,是一种键值对的数据结构。golang里面的map是无序存放的,在输出它的时候会乱序输出

m := make(map[string]int)
m["one"] = 1
m["two"] = 2

range

对于一个切片slice或者map来说,用range可以更使代码更简介

用range遍历时,第一个返回的值为索引,第二个返回的值为数据

nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
    sum += num;
    if num == 2 {
        fmt.Println("index :", i, ",num:", num)
    }
}
m := map[string]string{"a" : "A", "b" : "B"}
for k, v := range m {
    fmt.Println{k, v}
}

函数

用func声明,不同于其他语言的是名字在前,类型在后。并且可以返回两个变量

func 函数名 (变量a a的类型, 变量b, b的类型) (返回类型,可以返回两个值)

指针

相比C++指针的功能已经少很多,代表传入引用修改这个变量的值

结构体

类似C/C++,关键词是type

type user struct {
    name string
    password string
}

结构体方法:类似成员函数;实现的方法是将结构体变量放到函数前面,并加上小括号

func (u user) checkPassword(password string) bool {
	return u.password == password
}

错误处理

函数的返回值有两个,第二个返回值返回错误信息

func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil;
		}
	}
	return nil, errors.New("not found")
}

字符串操作

举例说明:

a := "hello"
strings.Contains(a, "ll")
strings.Count(a, "l")
strings.HasPrefix(a, "he")
strings.HasSuffix(a, "llo")
strings.Index(a, "ll")
strings.Join([]string{"he", "llo"}, "-")
strings.Repeat(a, 2) 		// hellohello
strings.Replace(a, "e", "E", -1) 	//hEllo
strings.Splist("a-b-c", "-")
string.ToLower(a)
string.ToUpper(a)
len(a)

字符串格式化(Printf):可以加%v打印任何变量,使用%+v、%#v打印更详细

Json处理

在每个字段的首字母都大写的结构体的字段后面加上 'json : "字段名"'

type userInfo {
	Name string
	Age  int `json:"age"`
	Hobby  []string
}

使用JSON.marshaler序列化,JSON.unmarshaler反序列化

时间处理

time.now() 获取当前时间,time.Date()构造带时区的时间, .UNIX()获取时间戳

在生成随机数时,需要“播种”,用到

rand.Seed(time.Now().UnixNano())

数字解析

f, _ := strconv.ParseFloat("1.234", 64)		// 1.234
n, _ := strconv.ParseInt("111", 10, 64)		// 111
n, _ := strconv.ParseInt("0x1000", 0, 64)	// 4096
n2, _ := strconv.Atoi("123")				// 整形123

实战

都按最终版本实现了一次

猜数游戏

用到的输入比较麻烦,是因为熟悉一下bufio这种方式

大致的流程:

// 生成随机数
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNum := rand.Intn(maxNum)
// 读入数据
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
guess, err := strconv.Atoi(input)

简化的读取方式:

var guess int
_, err := fmt.Scanf("%d", &guess)

标准化输入和输出不一样,输入不能使用%v

在线词典

v1:

利用网站,根据请求报文的cURL生成go代码

// 创建请求
client := &http.Clinet{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"good","user_id":"632f1a87342fe300152e1e24"}`) 	// 请求报文的一些信息
req, err := http.NewRequset("POST", URL, data)		// 请求报文的方法体,请求资源的信息
// 继续添加请求报文的字段信息,设置请求头
// ......

// 发起请求
resp, err := client.Do(req)
defer resp.Body.Close()			// 会在函数结束时候执行

// 读取响应
bodyText, err := iotil.ReadAll(resp.Body)	// 这里就是响应报文的信息了

v2:

需要让一个变量代替输入,需要用到JSON序列化

创建一个结构体:

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

用自定义的字段创建请求:

request := DictRequest{TransType : "en2zh", Source : "good", UserID : "xxxx"}
buf, err := json.Marshal(request)
var data = bytes.NewReader(buf)

代替上一段的创建请求

v3:

我们要解析响应报文,利用OkTools网站将json转换为go语言的结构体:

image-20230114232438654

添加结构体之后,我们最后只需要将bodyText这个json字符串反序列化成该结构体的字段就行

var dictResponse DictResponse
err = json.unmarshal(bodyText, &dictResponse)
fmt.Printf("#v", dictRespnse)

这样输出的信息就有了比较清楚的信息:image-20230114233236951

v4:

最后我们想要任意输入,最后输出它的中文解释

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

输入的话标准化输入、命令行输入都行

  • 添加另一种翻译引擎:按照上面的方法添加百度翻译等
  • 并行请求两个翻译引擎:使用goroutinue来完成

SOCKS5代理

image-20230115130144547

浏览器与socks5代理建立TCP连接,代理再和真正的服务器建立连接。

分为四个阶段:握手阶段、认证阶段、请求阶段、relay阶段

握手阶段与简单的socket通信类似,在process函数里面实现逻辑

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

认证阶段:

浏览器先给代理服务器一个包,这个包有三个字段:version:协议版本号;methods:认证的方法数目;每个method的编码。

实现auth认证函数,在process函数里调用

image-20230115135304484

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

把版本号读出来:

	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)
// readFill填充method
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
	log.Println("ver", ver, "methond", method)

请求阶段:

浏览器发送一个包,有6个字段:

image-20230115135248293

然后创建connect函数,在process函数里调用,代码和上面认证阶段读取报文差不多

读取版本号ver、Connect请求cmd、目标地址类型atyp:

buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
ver, cmd, atyp := buf[0], buf[1], buf[3]

判断目标地址类型:

  • IPV4:

    _, err = io.ReadFull(reader, buf)
    addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    
  • HOST:

    hostSize, err := reader.ReadByte()		
    host := make([]byte, hostSize)
    _, err = io.ReadFull(reader, host)
    addr = string(host)
    
  • IPV6:暂不支持

最后读取端口:

_, err = io.ReadFull(reader, buf[:2])
if err != nil {
	return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2])		// 大端字节序

relay阶段:

用net.dial建立一个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()

用io.Copy可以实现单向数据转发,双向需要启动两个goroutinue

_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
	_, _ = io.Copy(dest, reader)
	cancel()
}()

go func() {
	_, _ = io.Copy(conn, dest)
	cancel()
}()
<-ctx.Done()

完成后,打开两个终端,就完成了

go run chapter1/proxy/v4/main.go 
curl --socks5 127.0.0.1:1080 -v http://www.qq.com

个人觉得proxy还是很有难度的,读取报文段的字节那里不容易掌握。 通过上述的三个例子,基本上实现了其他语言到go语言的转化