这是我参与「第五届青训营 」笔记创作活动的第1天
本节课主体分为两部分:1.Go语言基本语法特性 2.Go语言基本应用实战
1.Go语言基本语法特性
在总体上,Go语言使用基本库和由使用基本库集成封装的外部库达到实现基本功能,这是相较C++和Java来说较为方便和简洁的地方
①标准输出:使用fmt库的Println函数:
fmt.Println("hello world")
②变量声明: var关键字声明变量,根据右值类型自动判断变量类型,也可以在变量后进行说明,也可以省略var关键字用:=
var a = "initial"
f := float32(e)
var b, c int = 1, 2
③循环与判断:
只有for一种循环方式,括号不被编译器要求:
for循环形式
while(1)可以直接被省略为for
for j := 7; j < 9; j++ {
fmt.Println(j)
}
while循环形式
for i <= 3 {
fmt.Println(i)
i = i + 1
}
if else写法与for写法基本一致,但是if必须要求语句块:
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
④数组方面go并不常用的静态数组与其他语言基本保持一致,使用方括号[]来下标索引,同时数组长度使用len()返回
⑤切片:
切片是Go语言中更加经常使用的数据结构,其动态特点支持append()、copy()、len()等操作。
声明一个切片数组使用make()函数,第一个参数[]代表声明的数组维度,之后声明存入的数据类型,第二个参数代表数组长度。
但是append函数由于使用一个指向数组的指针和预先分配的一块空间,可能遇到的剩余空间不足分配的情况,因此需要变量来接受返回值,从而应对重新创建一个切片对象的情况。
数组切分也可以使用下标区间的方式进行切分
s := make([]string, 3)
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
s = append(s, "d")
s = append(s, "e", "f")
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
⑥map类型:
map类型与切片相同,都使用make()函数进行,区别在于需要使用map[key_tyoe]val_type作为传入参数,其他使用方式与python的dict数据类型一致。
遍历map时不会按照插入顺序输出,而是会有一个偏随机的方式输出
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
⑦使用range遍历数组:
for i, num := range arr会返回两个迭代器,i为数组下标而num为对应下标的变量值,(不需要索引时可以使用下划线代替for _, u := range users):
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
⑧函数与指针: 函数与指针基本与其他语言大同小异,需要注意的一点是函数声明时变量类型需要后置,返回值类型则在整个函数后面后置,同时支持多个返回值:
func add(a int, b int) int {
return a + b
}
由于多个返回值的特性,Go语言可以为每一个函数添加error类型变量作为返回值,从而在异常情况下迅速找到出现错误的函数。
⑨字符串: 全部的字符串方法如下:
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
注意一个汉字可能会对应多个字符
⑩json与结构体的转换:
使用json.Marshal()将结构体转化为json数据流
(需要注意是字节流类型的数据,直接打印会得到二进制数值)
使用json.Unmarshal()将json数据流转化为定义的结构体类型,注意使用的结构体定义时必须有反单引号标记的变量所对应的json标签。
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
a := 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,"Hobby":["Golang","TypeScript"]}
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{"Golang", "TypeScript"}}
2.Go语言基本应用实战
①生成随机数
*rand类需要使用Seed()方法设定随机数的选取方式,否则只会得到不变的值
graph TD
构建reader类读取输入 -->
使用strconv类的Atoi方法将字符串转换为整数-->
根据输入进行判断并进行标准输出
需要注意在每一步都要使用err变量监控错误抛出
②在线词典
工具准备:翻译api与请求头的获取
api使用彩云小译提供的翻译api,在fanyi.caiyunapp.com/ 中使用网页开发者工具监听请求,查找到POST请求:
使用Copy as curl选项复制curl命令即可得到请求头的对应信息, 在curlconverter.com 可以将请求快速转化为代码形式
工具准备:json转结构体
在oktools.net/json2go 中粘贴网页开发者工具预览中的json对象,选择“转换-嵌套”即可生成接受json传参的结构体
graph TD
构建reader类读取输入+构造接受json传参的结构体 -->
发送请求接受json格式数据返回-->
进行标准输出
需要注意的是使用了http类进行请求发送和接受操作, 核心方法为:
client := &http.Client{}:构造发送请求的Client对象
var data = strings.NewReader(buf):buf为json.Marshall()方法返回的流式json,也可以用反单引号下的json格式代替,流式数据可以大幅减少内存占用
req,err := http.NewRequest("POST",url,data):构建请求
req.Header.Set(title,val):设定请求头
resp, err := client.Do(req):发送请求并接受返回response
defer resp.Body.Close():defer关键字标注的语句会进入栈中,在函数结束后递归执行,从而达到清空Body流的效果
bodyText, err := ioutil.ReadAll(resp.Body):读取Body流中的消息体流
err = json.Unmarshal(bodyText, &dictResponse):将流式数据存入事先准备好的结构体中
③SOCKS5代理服务器
SOCKS5协议是一种明文的代理协议,可以用于内网的前端机器向后端服务器的访问,即在隔离内网保证安全的同时提高部分人员的访问权限。
*go关键字的使用:go关键字可以类比为其它语言的多线程函数,但是go关键字启动的独立进程开销更小,可以轻松处理上万的并发。
*go字节流数据的读取机制:go使用ReadByte()循环读取字节流时会先读取一定大小的的数据,直到该数据流被读完,因此循环执行读一字节命令的开销小于多线程依次读取
*对多个线程的控制和终止条件选择:ctx, cancel := context.WithCancel(context.Background()),context.Background()方法为一个context的初始化方法,可以用于创建之后可以取消的context对象,而context对象则可以在不同的进程之间同步请求特定数据,当线程结束时调用cancel()方法,之后使用<-ctx.Done()向管道传入终止数据
与代理服务器建立连接过程
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]
'''
协议建立失败终止条件
'''
//与代理服务器建立TCP连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
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
}
协议验证过程
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
ver, err := reader.ReadByte()
methodSize, err := reader.ReadByte()
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
_, err = conn.Write([]byte{socks5Ver, 0x00})
return nil
}
处理连接过程
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
}
}
实际上三个方法仅仅实现了主机和代理服务器之间实时的tcp连接和数据传输,但是并没有实现目标端与代理服务器的传输,在这个问题上只需要在curl中指定--socks5参数即可选择以socks5协议进行代理,此时reader := bufio.NewReader(conn)即可获取正确的socks5报文,从而通过验证,完成数据传输。
个人总结
本节课从语言基本特性的角度讲起,介绍了go语言的基本特性,并利用go语言的特性,从标准输入输出、请求的发送json数据流的读取、面向连接的协议建立和多线程应用来展示具体实战中的代码编写方式和规范。整体全面且涵盖丰富,并且只使用本地库的特点也体现了go语言在兼容性方面出色的表现。