Golang基础 | 青训营笔记

50 阅读7分钟

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

Golang基础知识


Hello World

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello World")
}

main包是Golang的入口包,main函数是Golang入口函数。此处使用fmt中的Println函数进行输出。

变量

常见的变量类型有整数、浮点、布尔、字符串。

变量的定义有两种方式:

var a int = 1
var b = 2
c := 3

使用var定义变量时如果可以推导出类型就可以省略类型。定义常量时使用const关键字,且必须进行初始化。

运算符优先级与C语言基本相同,在此省略。

if语句

Golang中if-else语句与C语言大体相同,但是条件不使用()进行包含,且if后必须跟{

使用示例如下:

    if a == 0 {
            fmt.Println(a)
    }else {
            fmt.Println(b)
    }
    
    if a == 1 {
            fmt.Println(a)
    }else if b == 2 {
            fmt.Println(b)
    }else {
            fmt.Println(c)
    }

循环语句

Golang中仅有for一种循环,格式与C语言大体相同。可以使用breakcontinue进行操作。for后分为三段,三段中任意一段都可以省略,全部省略即为死循环。

使用示例如下:

    for {
            fmt.Println("Hello")
            break
    }

    for i := 0; i < 5; i++ {
            if i%3 == 0 {
                    continue
            }
            fmt.Println(i)
    }
    
    i := 0
    for i < 10 {
            fmt.Println(i)
            i++
    }

switch分支

Golang中switch分支格式与C语言类似。但是csae语句触发时只执行当前分支的内容,不会像C语言一样继续向下执行。而且功能更为强大,支持表达式和各种数据类型。

使用示例如下:

    a := 1
    switch a {
    case 1:
            fmt.Println(2)
    case 2:
            fmt.Println(4)
    default:
            fmt.Println(100)
    }

    switch {
    case a == 1:
            fmt.Println(2)
    case a > 5:
            fmt.Println(4)
    default:
            fmt.Println(100)
    }

数组

类似C语言中的数组,长度固定无法改变,使用方式相同,均使用索引访问。

使用示例如下:

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

切片

类似于可变长度的数组,同样通过索引直接访问。同时可以通过append函数进行扩充,copy进行拷贝。同时还满足类似python的切片操作。

使用示例如下:

    a := make([]int, 3)
    b := []int{1, 2, 3}
    copy(a,b)
    a[0] = 2
    a = append(a, 1, 2)
    // 0到2,不包括2
    a = a[:2]

map

代表一种映射关系。用键值对表示,使用建访问值。使用delete函数删除一个键值对。使用range进行遍历。

使用示例如下:

    a := make(map[int]string)
    b := map[int]string{1: "12", 2: "23"}
    a[1] = "a"
    a[2] = "b"
    
    delete(a, 2)
    
    //   false
    v, l := a[0]
    // a true
    v, l = a[1]
    
    for k, v := range a {
            fmt.Println(k, v)
    }

函数

函数定义使用func关键字。支持多返回值,且常用多返回值以表示是否出现错误。

使用示例如下:

func add(a int, b int) int {
	return a+b
}

func add2(a int, b int) (int,bool){
	return a+b,true
}

func add3(a int, b int) (c int,o bool){
	c = a + b
	o = true
	return c,o
}

指针

支持指针,但支持十分有限。不支持C语言中的指针运算。

使用示例如下:

    a := 1
    var b *int
    b = &a
    fmt.Println(*b)

结构体

定义与C语言类似。对属性的使用与C语言类似。但是Golang中结构体可以定义结构体方法,借助该方法体现了面向对象的思想。

使用示例如下:

type student struct {
    id int
    name string
}

func main() {
    a := student{id: 1, name: "ZhangSan"}
    b := student{id: 2}
    b.name = "LiSi"
    fmt.Println(a.equal(&b))
}

func (s student) equal(s2 *student) bool {
    return s.id == s2.id && s.name == s2.name
}

错误处理

与C语言和Java等都不同,使用返回值和if语句进行错误处理。使用panic进行错误打印

func main() {
    v, err := divide(1, 0)
    if err != nil {
        panic(err)
        return
    }
    fmt.Println(v)
}

func divide(a int, b int) (v int, err error) {
    if b == 0 {
        return 0,errors.New("divide by zero")
    }
    return a/b,nil
}

Golang实践练习


猜数游戏

要点一:随机数 使用math/rand包中的Intn方法获取随机数。但是需要使用Seed设置种子,否则每次获取的随机数都是相同的。

代码如下:

    maxNum := 100
    rand.Seed(time.Now().UnixNano())
    secretNumber := rand.Intn(maxNum)
    fmt.Println("The secret number is ", secretNumber)

要点二:输入

使用fmt包中的Scanln函数进行读取。

代码如下:

    fmt.Println("Please input your guess")
    guess := 0
    _, err := fmt.Scanln(&guess)
    if err != nil {
        return
    }
    if err != nil {
        fmt.Println("An error occured while reading input. Please try again", err)
        return
    }
    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!")
    }

最后将用户输入和游戏规则放入一个循环中游戏就完成了。

在线词典

要点一:发送请求并获取收到的请求

首先抓取彩云小译 - 在线翻译 (caiyunapp.com)的包,并获取请求url,然后在Convert curl to Go (curlconverter.com)中生成请求代码。

由于生成代码较多,此处省略。

要点二:定义请求的结构体并进行序列化

代码如下:

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

request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)

要点三:结果反序列化

定义与返回结果对应的结构体,由于结构体过于复杂,所以使用JSON转Golang Struct进行生成。

由于生成代码较多,此处省略。

反序列化部分如下:

    var dictResponse DictResponse
    err = json.Unmarshal(bodyText, &dictResponse)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%#v\n", dictResponse)

要点四:获取命令行参数

使用os.Args进行获取。

具体代码如下:

    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
    example: simpleDict hello
        `)
        os.Exit(1)
    }
    word := os.Args[1]

SOCLKS5

要点一:server测试

写一个简易的server测试是否正常。

server部分: 创建一个server侦听发送到该接口的信息并进行处理

代码如下:

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

处理部分: 写入控制台中:

代码如下:

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

要点二:SOCKS5初步实现协议-协商阶段

按照代理协议的第一部分协商的内容,使用代码实现解析与返回。并将处理部分的内容更改为协议第一部分。

代码如下:

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

    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.Println("ver", ver, "method", method)
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", 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)
    _, 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]
    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    if cmd != cmdBind {
        return fmt.Errorf("not supported cmd:%v", ver)
    }
    addr := ""
    switch atyp {
    case atypIPV4:
        _, err = io.ReadFull(reader, buf)
        if err != nil {
            return fmt.Errorf("read atyp failed:%w", err)
        }
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    case atypeHOST:
        hostSize, err := reader.ReadByte()
        if err != nil {
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
            return fmt.Errorf("read host failed:%w", err)
        }
        addr = string(host)
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
        return fmt.Errorf("read port failed:%w", 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版本,这里为0x05
    // REP Relay field,内容取值如下 X’00’ succeeded
    // 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: %w", err)
    }
    return nil
}

要点四:SOCKS5初步实现协议-连接阶段

与真正的目的服务器建立连接,并将数据从客户服务器拷贝到目标服务器,即数据转发。

代码如下:

//建立连接
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()
//数据转发
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

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

<-ctx.Done()

全部完成后SOCKS5代理服务器就实现完成了。

个人总结

在本次课程中,接触到了Go语言的一些基础语法,同时通过实战学会了Go语言的一些基本运用。虽然在实践第三部分中使用到了Go语言独有的线程机制,但是大体思路也和前几个实践的思路相同,都是实现简单功能,只是有一个不太熟悉的协议规范,导致看起来和实现起来比较复杂,但是大体上还是比较容易的。