Go 语言入门 | 青训营

133 阅读5分钟

Go 语言是一门高性能高并发,语法简单,具有完善工具链,可静态链接与快速编译,可跨平台以及支持垃圾回收的编程语言,在处理后端业务上具有显著优势。本文旨在记载 go 语言的入门使用,为以后复习提供参考。

选用编辑器

· VScode 主流全能编辑器,配置了 go 插件即可。

· GoLand 专门用于进行 go 语言开发的编辑器,支持代码补齐,语法修改,代码提示,自动引包等优势,用起来相当丝滑。

关于使用

· go 语言依靠 Module 进行项目管理,要合理的运行管理一个 go 项目需要在 main 所在根目录下设置 go.mod 文件,编译运行前记得执行命令 go mod tidy 以完善 go.mod 和生成 go.sum 的信息。

· 经过一番折腾,目前得出结论是如果要引用自定义包,那么自定义包放置位置需要在 GOPATH\src 对应目录下。或许有不用把项目放到 GOPATH/src 也能使用相对路径实现引包的方法,但是目前没有实践出来。

关于语法

输出打印

放在 fmt 包中,可用 fmt.Println(a ... any) 的方式打印。

如果不想处理其中错误信息,可以使用 fmt.Printf(string, a ... any) 可以打印一个字符串。

变量初始化

有两种方式:

  1. 采用 var 关键字,对给定变量初始化,初始化方式可以是直接赋值,也可以是指定初始化类型。
  2. 采用 target := (Type) value 的形式同时完成类型和值的初始化。

常量初始化采用关键字 const, 形如 const h string = "hello" 必须完成值的初始化。

For 循环

有几种使用方式:

  1. 什么都不加,直接开始,但是需要设置出口 break, eg:

    for {
        // Do something.
        break
    }
    
  2. 类似 C++ 的写法,但是不用括号,eg:

    for i := 1; i < 10; i++ {
        // Do something.
    }
    
  3. 单纯给一个判断,此时用法像是其它语言的 where, eg:

    for i <= 3 {
        // Do something.
        i = i + 1
    }
    

If-else 条件判断

使用方法和其他语言没什么不同,就是正常 if-else 逻辑

if JUDGE1 {
    // Do something.
} else if JUDGE2 {
    // Do something.
} else {
    // Do something.
}

但是有些不同点在于,条件判断可以同时完成某些初始化,eg:

if num := 9; num < 0 {
    // Do something.
} else {
    // Do something.
}

Switch-case

总体和 C++ 使用方式相近,不同点在于:

  1. 每一个 case 后面不用加 break 也可以实现结束这一个 case 后自动结束。
  2. case 的 key 不一定是常量,可以是一个运算量,也可以是多个值。

示例:

switch a {
    case 1:
    fmt.Println("1")
    case 2:
    fmt.Println("2")
    case 3, 4:
    fmt.Println("3,4")
    default:
    fmt.Println("other")
}
​
switch {
    case t.Hour() < 12:
    fmt.Println("less")
    default:
    fmt.Println("more")
}

数组操作

数组的定义十分简单,和普通变量定义没什么区别,只需要在定义时加上数组关键字,如

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

· 同时其也支持二维数组,如 var twoD [2][3]int

· 如果不定长度,可以先初始化再使用 make 函数进行内存分配,如

var a []Type
a = make([]Type, Size)

· 同时该数组也支持 C++ 中对 vector 容器的一些抽象接口实现,如 append 等。

· 关于数组遍历:

  1. 可以采用普通的 for 循环,即 for i := 0, i < len(arr), i++ { // ... }

  2. 可以使用 range 遍历,如

    for i, element := range Arr {
        // Do something.
    }
    

    同时注意:range 不止可以遍历数组,还可以遍历一般的迭代器,实现类似 C++ 的迭代器遍历功能,对 map 的 key-value 对也能遍历。

· 关于输出:函数 fmt.Println() 可以接受多个参数,已经对数组接口做了抽象,直接输出即可,这点比 C++ 抽象更强。

· 关于切片:类似 Python 中的切片思路,Go 对数组也实现了切片操作,方式和 Python 切片方式差不多, Arr[2:5] 表示切片提取其中的 2,3,4 号元素,即取首不取尾。而 [:3], [2:] 之类的切片分别表示从其实开始切片到给定位置,以及从给定起点切片到最后。

Map

map 是一类存储 key-value 键值对的容器,可以通过 Map[key] 的形式获取对应的值.

· 初始化方式有:

  1. 使用 make 函数初始化,如 m: make(map[keyType]valueType).
  2. 使用 var 关键字,如 var m = map[string]int{"one":1, "two":2}.
  3. 使用 := 方式,如 m := map[string]int{"one":1, "two":2}.

· 索引返回值还有一个是否获取的标志,即 value, ok := map[key] 其中 ok 在搜索到键值的时候为 true, 搜不到时为 false.

· 打印:直接使用 fmt.Println() 即可。

· 删除键值对:使用 delete 函数,如 delete(Mao, key).

· 遍历:使用 range 遍历,如

for k, v := range m {
    // Do something. k --> key, v --> value
}
for k : range {
    // Do something. k --> key
}

函数

Go 的函数定义也很简洁,返回值放在声明后面,形如

func f(a int, b string) (int, error) {
    // Do something.
}
func g(x, y float64) (z float64) {
    // Do something.
}

注意,此处的形参为拷贝传入,在函数中对形参的修改并不能改变原来的实参,如果需要改变原来实参,需要采用指针传入,如

func add(n *int) { // 如果 n 只是传入 int, 则原来传入值不改变
    *n += 2
}
​
func main() {
    n := 5
    add(&n) // n 需要取地址传入,给函数指针
    // Do something.
}

结构体

Go 提供了定义结构体的方式,可以实现不同类型的抽象封装,构造定义是 type 关键字,如

type user struct {
    name     string
    password string
}

初始化时可以按照顺序直接初始化每个元素,也可以用关键字初始化,如

a := user{"Harry", "12345"}
var b = user{name: "Harry", password: "12345"}

· 索引元素使用 . 关键字,可以实现查找和对元素的修改。

· 打印:使用 fmt.Println() 可以直接打印

· 关于初始化时的 tag 标签:当这个结构体要转化为一些特定输出(比如 gorm, json 等)的时候,其结构体的 key 和输出对应的 key 可能不一样,也可能不是所有结构都需要给定值写入等,此时可以采用 tag 标签让结构体的元素在转化时有标准转化形式,如

type user struct {
    // 要求结构体 key 首字母大写才被 json 写入,为了转为小写用 tag 修改
    Name     string `json:"name"`
    // 如果想要把 password 设置为可以不写入时初始化,加上 omiempty 的 tag 标识
    Password string `json:"password, omiempty"`
}

当然,此处写入时会涉及到一些零值不被读取的问题,要解决这个问题需要把结构体元素类型定义为指针类型,如 size *int 等。

· 结构体方法:形如 C++ 中的 class 概念,go 通过结构体方法实现了对结构体内部函数的统一调度,如

// 不可修改原结构体,只是使用元素
func (u user) checkPassword(password string) bool {
    return u.password == password
}
// 传入指针,可以对原结构体进行修改
func (u *user) resetPassword(password string) {
    u.password = password
}

调用方式:a.resetPassword("2048"). 类似 C++ 中的用法。

报错 error

go 提供了一个统一的报错形式 error 结构,可以处理统一的报错,一般使用放在函数返回的第二个参数,如

u, err := findUser(Some target)
if err != nil {
    // Handle error
}

String

字符串也是一个 go 封装好的结构体,使用方式和 C++ 差不多,有一些细微的使用接口差别,可读性很强,支持搜索,计数,大小写转换,拼接,替换,分片等一系列操作。

时间相关 time

go 也对调用时间封装了一个很好的包 time,支持很多功能:

  1. 获取当前时间:time.Now() // 20xx-xx-xx xx:xx:xx.xxxxxx +0800 CST m=+0.000087933 时间格式.
  2. 通过给定时间拼装成一个时间: time.Date(2023, 8, 20, 1, 25, 36, 0, time.UTC).
  3. 获取具体的年月日等:t.Year(), t.Month(), etc.
  4. 计算两时间插值:t2.Sub(t1).
  5. 转化时间为整数:time.Now().Unix() 转化为从1970年某个点开始到现在时刻的秒数。
  6. ... ...

类型转化

类型转化有主要有直接调用 Type(target) 的函数做强制转化或者使用 strconv 包进行转化。

· 对于一些简单的转化,比如 int 转为 int64 这种类型,可以直接转化,如

var a int = 1
var b int64
b = int64(a)

· 对于一些复杂的转化,需要使用 strconv 的函数(返回值中除了转换值还有个 error 表示是否转换成功),如

// string --> float64
strconv.ParseFloat("1.234", 64) // 1.234
// string --> int64
strconv.ParseInt("111", 10, 64) // 111, 第二个参数表示10进制,第三个参数表示 int64
strconv.ParseInt("0x1000", 0, 64) // 4096, 第二个参数为0则采用默认转换,0x 表示16进制
// string --> int
strconv.Atoi("123") // 123

总结

总的来说 go 语法对有 C++ 或其他语言基础的人来说很友好,基本上看上两三天就会了,但是其中的工具包使用和语法结构比较复杂,需要进行比较多的训练。