Go语言基础学习day1 | 青训营笔记

89 阅读7分钟

Go学习笔记

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

1、Go基础语言

什么是go语言?

1)高性能,高并发 2)语法简单

3)丰富的标准库 4)完善的工具链

5)静态链接 6)快速编译

7)跨平台 8)垃圾回收

1)搭建开发环境

1)安装Golang

2)选择一个适合的开发环境如vscode,Goland。或者用云开发环境如gitpod。

2)基础语法

2.1Hello World
package main //基础语法
import ( //导入包
    "fmt"
)
func main(){ //main函数
    fmt.Println("Hello World")
}
2.2变量

go语言是一门强类型语言,每一个变量都有自己的变量类型。常见的变量类型有:字符串型,整数,浮点型,布尔型。和c++类似。

    var a = "initial" //声明变量方式 var name string = ""
    var b, c int = 1, 2
    var d = true
    var e float64
    f := float32(e) //或者使用 变量 冒号 := 等于值 常量把var改成const 常量没有确定的类型,会根据上下文来自动确定类型
    g := a + "foo"
2.3选择、循环语句

选择语句有 if else 和 switch 语句 switch 后可以不加任何变量,循环语句只有 for 语句。

//选择语句有 if else 和 switch 语句 switch 后可以不加任何变量
//循环语句只有 for 语句
    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")
    }
    for{
        fmt.Println("loop")
        break
    }
    for j := 7; j < 9; j++ {
        fmt.Println(j)
    }
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }
2.4数组 map range

数组,map,及range函数都类似Python语言。

    var a [5]int //一维数组
    a[4] = 100
    fmt.Println(a[4], len(a))
        var twoD [2][3]int //二维数组
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    s := make([]string, 3) //业务中更常用切片
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println(s[2:5])
    fmt.Println(s[:5])
    fmt.Println(s[2:])
    m := make(map[string]int) //map内部完全无序
    m["one"] = 1
    m["two"] = 2
    fmt.Println(m)
    nums := []int{2, 3, 4}
    sum := 0
    for i, num := range nums { //类似python
        sum += num
        if num == 2 {
            fmt.Println("index", i, "num", num)
        }
    }
    fmt.Println(sum)
2.5函数
func exists(m map[string]string, k string) (v string, ok bool) { 
    //前面的括号是传入的数据及其类型,后面的括号的return的数据及其类型,写法类似c++
    v, ok = m[k]
    return v, ok
}
2.6指针
func add2ptr(n *int) { //类似c++ 
    *n += 2
}
2.7结构体 结构方法
type user struct { //结构体定义 同样类似c++
    name     string
    password string
}
func (u *user) resetPassword(password string) { //结构方法
    u.password = password
}
2.8错误处理

go语言中习惯的错误处理方法是使用一个单独的返回值来传递错误信息。使用简单的if else 来处理错误。

    u, err := findUser([]user{{"wang", "1024"}}, "wang")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(u.name)
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") //返回错误信息
2.9字符串操作 字符串格式化

string包里有很多实用的处理字符串的工具函数。字符串格式化有很多方法,可以用类似c语言中printf() 的方式来格式化字符串。go语言可以用%v来打印任意类型的变量,%+v打印详细结果,%#v则是最详细的结果。

    a := "hello"
    fmt.Println(strings.Contains(a, "li")) //标准库中好用的函数
    fmt.Println(strings.Count(a, "l"))
    fmt.Println(strings.HasPrefix(a, "he"))
    fmt.Println(strings.HasSuffix(a, "llo"))
    fmt.Println(strings.Index(a, "li"))
    fmt.Println(strings.Join([]string{"he", "llo"}, "-"))
    fmt.Println(strings.Repeat(a, 2))
    fmt.Println(strings.Replace(a, "e", "E", -1))
    fmt.Println(strings.Split("a-b-c", "-"))
    fmt.Println(strings.ToLower((a)))
    fmt.Println(strings.ToUpper(a))
    fmt.Println(len(a)) 
    fmt.Printf("s=%v\n", s) //s=hello
    fmt.Printf("n=%v\n", n) //n=123
    fmt.Printf("p=%v\n", p) //p={1 2} //输出任意类型变量
    fmt.Printf("p=%+v\n", p) //p={x:1 y:2} //次详细的方式
    fmt.Printf("p=%#v\n", p) //最详细的方式 p=main.point{x:1, y:2}
​
    f := 3.141592653
    fmt.Println(f) //3.141592653
    fmt.Printf("%.2f\n", f) //3.14
2.10 json处理

对于一个结构体,只需要保证每个字段的第一个字母是大写的,也就是公开字段。那么这个结构体就能用JSON.marshal()去序列化,变成一个json字符串,系列化后的字符串也能用JSON.unmarshal()去反序列化到一个空的变量里面。这样默认序列化出来的字符串的风格是大写字母的,可以在后面用json tag等语法修改输出json结果里面的字段名。

    a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
    buf, err := json.Marshal(a) //去序列化
    if err != nil {
        panic(err)
    }
    fmt.Println(buf)
    fmt.Println(string(buf))
​
    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)
2.11 时间处理 数字解析

time.Now()获取当前的时间,也可以用time.date() 去构造一个带时区的时间,可以用sub得到两个时间点的时间差,用.UNIX()获取时间戳。数字解析则用strconv包下的函数,将字符串转化为数字。

    now := time.Now()
    fmt.Println(now)
    t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
    t1 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
    fmt.Println(t)
    fmt.Println(t.Year(), t.Month(), t, t.Day(), t.Hour(), t.Minute())
    fmt.Println(t.Format("2006-01-02 15:04:05"))
    diff := t1.Sub(t)
    fmt.Println(diff)
    fmt.Println(diff.Minutes(), diff.Seconds())
    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()) //时间戳
f, _ := strconv.ParseFloat("1.234", 64) //strint convert to float64 下面同理
    fmt.Println(f) //1.234 
​
    n, _ := strconv.ParseInt("111", 10, 64)
    fmt.Println(n) //111
​
    n, _ = strconv.ParseInt("0x1000", 0, 64)
    fmt.Println(n) //4096
​
    n2, _ := strconv.Atoi("123")
    fmt.Println(n2) //123
2.12进程信息

go语言中可以用os.argv来得到程序执行的时候的指定的命令行参数。

    fmt.Println(os.Args)
    fmt.Println(os.Getenv("PATH"))
    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))

2、GO工程实践

猜数字游戏

用时间戳初始化种子生成随机数,在死循环中判断用户输入数字和随机数的大小关系,最后如果等于随机数则退出。

func main() {
    maxNum := 100
    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)
    for {
        input, err := reader.ReadString('\n')
        if err != nil {
            fmt.Println("An error occured while reading input. Please try again")
            continue
        }
        input = strings.Trim(input, "\r\n")
        guess, err := strconv.Atoi(input)
        if err != nil {
            fmt.Println("Invalid input. Please enter an integer 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
        }
    }
}

在线词典

运用第三方api以及代码生成工具,对生成代码解读,解析reaponse body,最终打印目标结果。

代码主体由结构体DictRequest和DictResponse,query函数和main函数组成

func query(word string) {
    client := &http.Client{}
    // var data = strings.NewReader(`{"trans_type":"en2zh","source":"go"}`)
    request := DictRequest{TransType: "en2zh", Source: word}
    buf, err := json.Marshal(request)
    if err != nil {
        log.Fatal(err)
    }
    var data = bytes.NewBuffer(buf)
    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", "en,zh-CN;q=0.9,zh;q=0.8,en-US;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="8", "Chromium";v="108", "Google Chrome";v="108"`)
    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/108.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 := ioutil.ReadAll(resp.Body) //读取响应
    if err != nil {
        log.Fatal(err)
    }
    if resp.StatusCode != 200 { //判断response的正确性
        log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
    }
    // fmt.Printf("%s\n", bodyText) //json字符串
    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &dictResponse)
    if err != nil {
        log.Fatal(err)
    }
    // fmt.Printf("%#v\n", dictResponse) //%#v最详细方式打印
    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)
}

最终效果:

Screenshot 2023-01-15 202551.png

socks5代理服务器

socks5:

SOCKS5 是一个代理协议,在TCP/IP协议通讯中的前端及其和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。简而言之,socks5可以使授权用户通过单个端口访问内部所有资源,从而避免严格的防火墙带来的副作用。

socks原理:

image.png

正常浏览访问一个网站,不经过代理服务器的正常流程为:先和对方的网站建立TCP连接,然后三次握手,握手完之后发起HTTP请求,之后服务返回HTTP响应。

而设置代理服务器流程会变得复杂一些:浏览器和socks5代理建立TCP连接,代理再和真正的服务器建立TCP连接。可以分为四个阶段:握手阶段、认证阶段、请求阶段、realy阶段。

一、握手阶段。 浏览器会向socks5代理发送请求,包的内容包括一个协议的版本号,支持认证的种类,socks5服务器会先选中一个认证方式,返回给浏览器。

二、认证阶段。 如果返回的是00的话代表不需要认证,返回其他类型会开始认证流程。

三、请求阶段。 认证通过后浏览器会向socks5服务器发起请求。主要信息包括版本号,请求的类型,一般主要是connection请求,就代表代理服务器和某个域名或者某个IP地址某个端口建立TCP连接。代理服务器会和真正的后端服务器建立连接,然后返回一个响应。

四、relay阶段。 此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后真正的服务器返回响应也会把请求转发到浏览器这边。

TCP echo server

首先写一个简单的TCP echo server,完成一个能够返回输入信息的一个TCP server。

func main() {
    server, err := net.Listen("tcp", "127.0.0.1:1080") //监听端口,会返回一个server
    if err != nil {
        panic(err)
    }
    for { //进入死循环
        client, err := server.Accept()
        if err != nil {
            log.Printf("Accept failed %v", err)
            continue
        }
        go process(client) //启动一个goroutinue
    }
}
func process(conn net.Conn) {
    defer conn.Close()              //函数退出时要把连接关掉,避免资源泄露
    reader := bufio.NewReader(conn) //创建一个带缓冲的只读流,减少底层系统调用的次数
    for {
        b, err := reader.ReadByte() //readbyte读取单个字节,再把这一个此节写进去连接
        if err != nil {
            break
        }
        _, err = conn.Write([]byte{b})
        if err != nil {
            break
        }
    }
}

此时就可以简单做一个测试,运行go程序后,可以在终端中输入

nc 127.0.0.1 1080

即可得到输入的内容。(nc为netcat,为Linux自带命令,Windows下需要下载一个程序后才可使用。)

Auth

添加以下内容。

const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
    //分为三个字段
    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)
    }
    log.Panicln("ver", ver, "method", method)
​
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

请求阶段

试图读取到 携带 URL 或者 IP 地址+端口的包,然后把它打印出来。实现一个connect函数,同样在process里面去调用。浏览器会发送一个包,包里面包括六个字段:1、version版本号;2、command,代表请求的类型,我们只支持connection请求,也就是让代理服务建立新的TCP连接;3、RSV保留字段,不理会;4、atype,目标地址类型,可能是IPV4或者IPV6;5、addr,地址长度根据atype的类型不同而不同。6、port端口号,两个字节,我们需要逐个去读取这些字段。

relay阶段

标准库的io.copy可以实现一个单向数据转发,双向数据转发需要启动两个goroutinue。还有一个问题为connect()函数会立刻返回,返回后即关闭。我们需要等待任意一个方向copy出错的时候再返回connect()函数。这里可以用标准库中的一个context机制,用context连接with cancel来创建一个context。最后等待ctx.Done(),只要cancel被调用,ctx.Done()就会立刻返回,然后再上面的goroutinue里面调用一次cancel即可。

最终测试

运行程序再执行curl命令可以得到成功返回的内容。或者在switchomega插件中得到代理服务器里显示的浏览器版本的域名和端口。

day1总结

青训营的课程节奏比较快,不过有了其他语言的基础学习go语言还是比较容易的,第一天学习了go的语言特性,基础语法,并用go语言开发了三个小工程,凸显了go语言使用的场景,对于go语言由丰富的标准库,完善的工具链,高性能,高并发的的特性。小工程代码量不多的情况下实现了很实用的功能。相比于其他语言有很多实用函数需要学习,也可以更好面对很多应用场景,所以学习难度也会加大,需要多敲代码熟悉语言功能。