过完年写GO的笔记-基础语法篇 | 青训营笔记

77 阅读5分钟

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

前言

之前做笔记都是用语雀,稀土这个还是第一次用。 linux系统可以参考官网安装go环境。 windows系统可以使用GoLand。

我的开发环境是windows系统+vscode+远程linux服务器。 Goland也有,毕竟在校生有免费的可以用,而且看代码很方便。

基础语法

变量

golang中变量的声明与c语言的不同是,golang中的变量类型是放在变量名的后面,是后置的。

两种变量声明方式,var 和 :=,var功能上类似于c++中的auto。常量是把ver改成const。

var a = "asdads"
f := float32(0)
const a string = "asda"

数组

var a [5]int

//数组长度
len(a)

// 初始化列表
b := [5]int{1, 2, 3, 4, 5}

// 二维数组
var twoD [2][3]int

切片

切片是可变数组。使用make创建数组

s := make([]string, 3)

//append追加元素,必须要赋值回去,因为发生扩容之后会返回一个新的slice
s = append(s, "d")

// copy拷贝slice
c := make([]string, len(s))
copy(c, s)

// 相当于python中的切片操作
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5])  // [a b c d e]
fmt.Println(s[2:])  // [c d e f]

条件|循环|switch|range

条件和循环没啥好说的,和其它语言非常相似,只是在基础语法上有所区别。

switch和C++相比不需要在每条分支后加break,但是也具备了break的功能。另外,go中的switch功能很强大,可以使用任意变量类型,而不是局限于整数的常量表达式。此外go中switch也可以用作条件语句。

t := time.Now()
switch {
case t.Hour() < 12:
        fmt.Println("It's before noon")
default:
        fmt.Println("It's after noon")
}

range有点像c++中的范围for,但其实应该是更像python中for的用法。 range返回两个值 第一个是key_type 第二个是value_type。 不需要的可以使用占位符‘_’占位。

nums1 := []int{2, 3, 4}
// 如果占位符是在value_type的位置的话,可以省略
// for i := range nums1 { 
for i, _ := range nums1 { 
        fmt.Println("index:", i) // index: 0 index: 1 index: 2
}

map

使用make创建map,[key_type]value_type。

添加元素:go中map的添加元素和c++基本一致,可以使用键值索引来增加元素。

同样map也可以使用初始化列表,go中的map是无序的。

// 使用make创建map,[key]value;
m := make(map[string]int)

// 添加元素:go中map的添加元素和c++基本一致,可以使用键值索引来增加元素
m["one"] += 1;

//删除元素
delete(m, "one")

//判断元素是否存在
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false

//初始化列表
m2 := map[string]int{"one": 1, "two": 2}

函数|结构体|成员函数

函数格式 func 函数名(变量名 变量类型, args...) (返回值名 返回值类型, args...){}

函数格式 func 函数名(变量名 变量类型, args...) (返回值类型, args...){}

第一种方法相当于创建了返回值变量。

func exists(m map[string]string, k string) (v string, ok bool) {
	v, ok = m[k]
	return v, ok
}

go中的实参传递分为值传递和指针传递,没有c++中的引用。指针传递和c中是一致的。

结构体和c++中的类似,需要注意的是golang中变量类型后置的问题,以及基本语法的差异。非成员函数与c++中的一致,需要把结构体对象作为函数形参。成员函数(go中叫结构体方法)与普通函数在格式上有所区别。

//结构体
type user struct {
	name     string
	password string
}

//非成员函数
func checkPassword(u user, password string) bool {
	return u.password == password
}

//成员函数
func (u user) checkPassword(password string) bool {
	return u.password == password
}

//指针传递
func (u *user) resetPassword(password string) {
	u.password = password
}

错误处理

函数返回两个值,第一个是真的返回值,第二个参数是error。

同时,golang中的error和c++中的异常不一样,c++的异常发生后,函数会中断。 golang则不会,golang中的错位需要自己去处理。

字符串操作

很多字符串函数的用法可以直接从名字中看出。

Contains	是否包含
Count		计数
HasPrefix       判断头substr
HasSuffix	判断尾substr
Index		相当于find_first_of
Join([]string{"he", "llo"}, "-")	相当于c语言中strtok的反面
Repeat		赋值粘贴,重复多个字符串
Replace		替换
Split		与strtok相近
ToLower		小写,c++中也有类似的,不过c++中是对字符的操作
ToUpper		大写
len		长度

例程:

package main
import (
	"fmt"
	"strings"
)
func main() {
	a := "hello"
	fmt.Println(strings.Contains(a, "ll"))                // true		
	fmt.Println(strings.Count(a, "l"))                    // 2			
	fmt.Println(strings.HasPrefix(a, "he"))               // true
	fmt.Println(strings.HasSuffix(a, "llo"))              // true
	fmt.Println(strings.Index(a, "ll"))                   // 2 			
	fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
	fmt.Println(strings.Repeat(a, 2))                     // hellohello
	fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
	fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
	fmt.Println(strings.ToLower(a))                       // hello
	fmt.Println(strings.ToUpper(a))                       // HELLO
	fmt.Println(len(a))                                   // 5
	b := "你好"
	fmt.Println(len(b)) // 6
}

字符串格式化

可以使用%v打印任意类型,比c更简单方便。

s := "hello"
fmt.Printf("s=%v\n", s)

%+v 		//得到更详细的结构
%#v		//进一步详细

%.2f		//打印两位小数的浮点数

字符串转换

字符串转数字和数字转字符串,需要包含strconv库

strconv.Atoi		//字符串转数字
strconv.Itoa		//数字转字符串

//输入不合法,第一个返回值返回0,第二个返回值获取错误信息

json操作

json是我第一次接触,以前没学过java。

go中使用Marshal和Unmarshal进行序列化和反序列化。

使用json,要求结构中的变量名的首字母大写,否则序列化之后无法打印,即没有被序列化。同时反序列化之后,首字母小写的变量名会被还原(即结构体成员是都有的),但是初始化的值没有,显示为nil。

使用Marshal强转后,如果要输出序列化后的结果,则需要用string方法再强转一下,否则输出的是数字。

例程:

package main

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name  string
	Age   int `json:"age"`
	hobby []string		//这里首字母设置的是小写
}

func main() {
	a := userInfo{Name: "wang", Age: 18, hobby: []string{"Golang", "TypeScript"}}
	fmt.Printf("%#v\n", a)
	//main.userInfo{Name:"wang", Age:18, hobby:[]string{"Golang", "TypeScript"}}
        buf, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18}

	buf, err = json.MarshalIndent(a, "", "\t")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf))

	var b userInfo
	err = json.Unmarshal(buf, &b)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b) 
    // main.userInfo{Name:"wang", Age:18, hobby:[]string(nil)}
    //hobby:[]string(nil) 的输出为nil
    // 如果是大写,则为
    // main.userInfo{Name:"wang", Age:18, hobby:[]string{"Golang", "TypeScript"}}
    //所以使用Marshal的结构体的变量名必须为大写
}

时间处理

go中有很多时间处理的函数,具体可以查看time包。

time.Now()	//获取当前时间
time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC) //创建一个标准时间(年月日时分秒)
t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()	//根据标准时间,返回信息
t2.sub(t);		//返回t2,t1标准时间的差值,返回值为时分秒
diff.Minutes(), diff.Seconds()	//将(时分秒)时间转换为(总数)分,秒
t.Format("2006-01-02 15:04:05") //格式化标准时间为一个字符串
time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36") //将后者解析成时间
时间.Unix()	//获得时间戳

go语言实战

这一节主要是关于课中所讲的一些知识点的总结。

随机数

"math/rand"包

"time"包

随机数步骤:

1、设置随机数种子

2、设置随机数

例程:

package main

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

func main() {
	maxNum := 100
        //获得当前时间的纳秒级时间戳
	rand.Seed(time.Now().UnixNano())
        //获得一个[0,maxNum)的整数随机数
	secretNumber := rand.Intn(maxNum)
	fmt.Println("The secret number is ", secretNumber)
}

文件输入流

类似c++中的iostream类

需要导入包"bufio"

os.Stdin指向标准输入文件/dev/stdin,即os.Stdin是标准输入文件/dev/stdin的指针。个人理解是类似于标准文件描述符,新建os.Stdin流相当于创建了一个对标准输入流的监听。

//新建流
reader := bufio.NewReader(os.Stdin)	

//从流中读取一行,按行读取
input, err := reader.ReadString('\n')

//按行读取包括了换行符,所以去掉换行符
input = strings.Trim(input, "\r\n")

//转为数字
guess, err := strconv.Atoi(input)

上述代码实现了从标准输入流中读取数字。

也可以使用fmt包中的Scanf函数从os.Stdin中进行读取。

var i int
var name string
fmt.Scanf("%d", &i) 
fmt.Scanf("%s", &name)

代码生成

将复制的CURL代码,通过代码生成网站自动转为go代码: curlconverter.com/go/

将json转为golang struct: oktools.net/json2go

抓包

基本思路就是创建一个http客户端,向目标网站发起链接请求,然后从目标网站返回的序列当中反序列出我们需要的内容。

当我们进行http请求的时候,需要设置请求行,请求头,请求体这些。上面的代码生成网站就是用来生成请求头的。

抓包方法:chrome浏览器F12打开开发者工具,复制cURL代码,然后在代码生成网站进行转换。

image.png

image.png

得到生成代码如下:

package main

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

func main() {
	client := &http.Client{}
	var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
	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")
	req.Header.Set("app-name", "xy")
	req.Header.Set("content-type", "application/json;charset=UTF-8")
	req.Header.Set("origin", "https://fanyi.caiyunapp.com")
	req.Header.Set("os-type", "web")
	req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
	req.Header.Set("sec-ch-ua", `"Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"`)
	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/105.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)
}

得到一个简单的http客户端代码。但这里最后的输出是序列化的结果,我们还需要反序列化,并选择我们需要的结果,等等操作。

反序列化需要用到结构体,上面的代码转换工具也提供了相应的JSON结构体转换。

SOCKS5代理

代理服务器在流程上很清晰,但是在具体实现上还是需要多看一看。

image.png

代理服务器socks5流程:

1、协商\握手

2、认证(我们是免密的,所以直接返回00就行)

3、请求

4、relay(数据交换)

在我们的代码中,握手和认证是一起进行了,请求和relay也是一起的。

接听阶段:

代理服务器打开tcp的一个端口,接收来自其它ip的请求。这里我设置成了任意ip可访问,而不是本机127.0.0.1,原因是我的代码是在云服务器上,即把云服务器做成了代理服务器,客户端则是自己的windows本机。

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

握手|认证阶段:

请求|replay阶段:

上面两个阶段有很大的篇幅是关于如何去解析客户端发来的报文和如何向客户端回复报文,这主要是与文件流读取以及格式校对有关,这里不再叙述,需要的话可以查看课程提供的代码。

在请求阶段,从客户端发来的报文中解析出目标ip和端口后,使用net.Dial函数,向目标服务器建立链接。

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)

然后实现数据转发, 使用copy函数实现代理服务器的转发功能,将客户端套接字,转发给目标服务器套接字。 然后再使用一次copy实现目标服务器到客户端的转发。

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

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

<-ctx.Done()

context机制:等待goroutinue执行完。只要cancel被调用,ctx.Done就是立刻返回,创建的两个goroutinue只要有一个执行即可,即只要有一方断开就结束。

最后介绍一下如何测试代理服务器,使用chrome的SwitchyOmega插件,我这里设置的代理服务器地址是云服务器的公网ip地址。测试结果运行正常。

image.png

image.png