Go语言基础语法-入门(近万长文)| 青训营笔记

324 阅读30分钟

这是参与「第五届青训营 」伴学笔记创作活动的文章,作为这段时间的学习笔记

Go 语言安装及配置

G0 语言特点

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

从业务维度看,go 语言已经在云计算、微服务、大数据、区块链、物联网等领域发展迅速,并且云计算、微服务等领域有着非常高的市场占有率。Docker 等大多数的云原生组件也是用 go 实现的。

安装

官网:golang.org

如果无法访问上述网址,可以用国内的下载地址:https:/studygolang.com/dl

  1. 下载对应平台安装包,安装即可

  2. 由于有时候 github 访问的速度会比较慢,所以还得配置 go mod proxy ,这样下载第三方依赖包的速度可以快很多,官网的详细配置方法:goproxy.cn/ (这一步必须做,不然后面会很烦)

配置开发环境

可以使用 vscode 或者 Goland,这里我演示一下 Goland 配置 goimports (写代码会比较智能一点)

  1. 下载好 Goland 之后,新建项目,然后鼠标移动到左上角的“文件”选项,在弹出的下拉菜单中选择设置。
  1. 在工具的下拉菜单中找到“File Watcher”,点击 + 号,选择 goimports ,之后一直点确定就行。
image.png

基础语法-Hello World

接下来就是程序员的经典入门案例

package main

import "fmt"

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

运行

image.png image.png image.png

代码解释:

package 是包,后面跟包的名字

import 是用来导入 go 语言的一些库,有了这些库之后,就可以实现一些很强大的功能

func 是 go 语言创建函数的关键字,后面跟函数名,带参数和有返回值的函数之后的笔记再说,这篇笔记主要是 go 的介绍和安装。

fmt.Println("Hello World") 就是调用 fmt 库的 Println 方法打印 Hello World 。(注意,go 语言的代码后面并没有分号,这也是一个特点)

变量

Go 语言的变量的定义

  1. 首先我们得知道,go 语言定义的变量是有初始值的,例如 int 类型初始值为 0,string 类型初始值为空字符串
  2. 其次,go 语言没有全局变量,在函数的外部定义的变量,作用域为包内而不是全局

Go 的自建变量类型

boolstring

(u)int, (u)int8, (u)int16, (u)int32, (u)int64, uintptr (前面的 u 加上则代表无符号类型,uintptr 是指针)

byte ,rune

float32, float64, complex64, complex128

特点:编译器可推测变量类型,没有 char,只有 rune

complex 是复数类型,例如 3+4i

注:complex64 的实数和虚数部分都是 float32,complex128 则是 float64

作用域在函数内部的变量

  1. var 是定义变量的关键字,后面跟变量名,然后是变量的类型

var 变量名 变量类型

func one() {
    var a int    // 初始值为0
    var b string // 初始值为空字符串""
    fmt.Printf("%d %q\n", a, b)
}

注:fmt.Printf("%d %q\n", a, b)和 c 语言很像,可以按一定的格式打印,%d 和%q 是占位符,%q 可以使字符串的双引号也一起输出,这样就可以看到控制台打印的空字符串了

  1. go 语言可以一次定义多个变量,定义的同时亦可以赋值
func two() {
    var a, b int = 3, 4
    var c string = "abc"
    fmt.Println(a, b, c)
}
  1. go 语言定义变量时可以不用写类型,会自动识别赋值的类型

例如下列代码的变量类型分别为 int int bool string

func three() {
    var a, b, c, d = 3, 4, true, "abc"
    fmt.Println(a, b, c, d)
}
  1. go 语言在函数内部定义变量时可以省略var,只需要在赋值的等号前面加上冒号“ : ”即可
func four() {
    a, b, c, d := 3, 4, true, "abc"
    b = 5
    fmt.Println(a, b, c, d)
}

作用域在包内的变量

可以直接在函数的外部写,大致上和函数内部差不多

但是注意,函数外部定义变量必须带有关键字 var ,错误写法: cc:=6

和函数内部一样,函数外定义的变量也可以自动识别赋值的类型

var aa = 5
var bb bool = true

由于函数外部 var 是必须的,但一堆 var 可能会影响观感,所以还可以这样定义

var (
    dd = "as"
    ee = false
)

看起来好多了

类型转换以及常量和枚举类型

关于类型转换

go 语言只有强制类型转换,没有隐式转换

以下代码是计算 3 的平方+4 的平方然后开根号的结果,math.Sqrt 函数的功能是开根号

var a, b int = 3, 4
var c int
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)

但是由于math.Sqrt的参数是float64,而a*a + b*b的结果为int,因为 go 不能隐式转换,所以得将结果强制转为float64,也就是float64(a*a + b*b)

又因为math.Sqrt的返回值也是float64,而我们规定用来接收结果的参数类型为int,所以还得将结果转为int,也就是int(math.Sqrt(float64(a*a + b*b)))

在部分的其他编程语言中,高精度的类型转低精度的类型可以直接隐式转换,而 go 得强转才行。这也是 go 语言的一个严格的地方(对以后的开发有好处)

pass: float 的小瑕疵:由于 float 在所有语言都不准确,所以当数据大一些的时候可能会出现例如正常算出来应该是 5,但 float 是 4.xxx,转成 int 之后则变成了 4 的情况

常量

和变量类似,常量也可以直接在函数外定义作用域同样是包内,而且和语法也和变量差不多,基本上都能通用

const filename = "abc.txt"
func consts() {
    const (
        a, b = 3, 4 // 注意这里(解释在下面)
        d    = "ddd"
    )
    c := int(math.Sqrt(a*a + b*b)) // 还有这里(解释在下面)
    fmt.Println(c, d, filename)
}

const 数值可作为各种类型使用,所以上述代码标记的第二个地方的地方不用强转为float64,系统自动判断为float64类型了

枚举类型

go 语言没有枚举关键字,所以一般用const来定义

const (
   aaa = 1
   bbb = 2
   ccc = 3
)
fmt.Println(aaa, bbb, ccc)

输出结果: 1 2 3

枚举类型还可以用iota表示这组const自增值,如果想跳过某个自增值,则可以用下划线“_”来跳过哦

例如:

const (
    cpp = iota // iota代表这组const是自增值
    _          // 可以用_跳过一个值
    java
    python
    golang
)
fmt.Println(cpp, java, python, golang)

输出结果: 0 2 3 4

iota 还可以参与运算

const (
   b = 1 << (10 * iota)
   kb
   mb
   gb
   tb
   pb
)
fmt.Println(b, kb, mb, gb, tb, pb)

输出结果:1 1024 1048576 1073741824 1099511627776 1125899906842624

条件语句

if 语句

首先得知道,go 语言的 if 的条件不需要括号

例如下列代码, filename 是一个 string 类型的常量,ioutil.ReadFile(filename) 则会返回两个值,一个是 byte 类型的数组,另一个是错误信息(如果没有错误则会返回 nil ,也就是 go 语言的 null

func iff() {
    const filename = "abc.txt"
    contents, err := ioutil.ReadFile(filename)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Printf("%s\n", contents) // 这里的contents是一个byte数组
    }
}

此时,如果没有 abc.txt 这个文件,则会打印出 open abc.txt: The system cannot find the file specified.。如果有,则会打印 abc.txt 文件中的内(注:ReadFile 的返回值是一个 byte 数组,可以用%s 占位打印出来)

以上代码还可以简略写为:

if contents, err := ioutil.ReadFile(filename); err != nil {
    fmt.Println(err)
} else {
    fmt.Printf("%s\n", contents)
}

也就是直接将计算或赋值的步骤放到了 if 条件中,用分号和后面的判断条件隔开。

语法为:if 赋值或计算语句 ; 判断条件 { 成功 } else { 失败 }

switch case 语句

与 if 语句类似, switch 语句的条件也不需要写括号。不过需要注意的一点是,与其他编程语言不同,go 语言的 switch 会自动 break ,除非使用 fallthrough

直接看以下示例代码吧:

func grade(score int) string {
    g := ""
    switch {
    case score < 60:
        g = "E"
    case score < 70:
        g = "D"
    case score < 80:
        g = "C"
    case score < 90:
        g = "B"
    case score <= 100:
        g = "A"
    default:
        panic(fmt.Sprintf("错误分数:%d", score))
    }
    return g
}

注:写在 default 语句中的 panic 可以让程序停下来并报错

如果执行 fmt.Println(grade(99)) 则会返回输出 A

但如果执行 fmt.Println(grade(999)) 则会输出报错 错误分数:999,以及错误原因(例如文件名以及错的行数等)

循环和指针

循环

go 语言的循环只有一个—————— for 循环,而且与 go 的条件语句类似,for 循环的条件也不需要加括号。

一个基础的 for 循环

for i := 0; i > 5; i++ {
    fmt.Println(i)
}

无限循环

由于 while 循环可以用 for 循环代替,所以 go 就干脆取消了 while 循环。例如无限循环

for true {
    fmt.Println("无限循环")
}

上面的代码还可以这样写(省略 true)

for {
   fmt.Println("无限循环")
}

指针

在编程语言中,调用函数时参数往往有两种传递方式:值传递和引用传递

值传递

下列代码的功能是交换传进的两个参数的值。注:参数为两个 int 类型的数据

func change(a, b int) {
    b, a = a, b
}

此时 a, b 的值是复制到函数中运行的,也就是说如果在主函数中定义 a,b :=3,4 运行 change(a,b),最后打印 a,b 的值则会发现 a 和 b 的值并没有被交换

因为函数中的值是由 a 和 b 复制过来的,无论怎么改变都不会对原本的 a 和 b 造成影响。

引用传递

下列代码的功能同样是是交换传进的两个参数的值。注:参数为两个 int 类型的指针

func swap(a, b *int) {
    *b, *a = *a, *b
}

此时 a,b 的值直接传递到函数中运行的,如果在主函数中定义 a,b :=3,4 运行 change(&a,&b),最后打印 a,b 的值则会发现 a 和 b 的值被成功的调换了

因为这个函数的参数是 int 类型的指针,传进的参数是 a 和 b 的地址,这样修改的就是原本的参数了(有点类似于顺着网线找到了你家的地址,然后强硬的让你点了个赞)

函数

一个参数,无返回值

go 语言中的函数声明用的是func关键字,定义一个参数,无返回值函数时的格式为

func 函数名(参数1 参数类型) {
    函数实现的功能
}

例子:

func printAll(a string) {
    fmt.Println(a)
}

上述代码的功能是打印传进来的字符串,调用的时候只需在main 函数中调用printAll("打印的字符串")即可

多个参数,有返回值

定义一个参数,有返回值函数时的格式为

func 函数名(参数1 参数类型, 参数2 参数类型) (返回值 返回值类型) {
    函数实现的功能
}

当返回值只有一个时,返回值的括号也可以不用写,只需写一个返回值的类型(其实返回值的名称是可以省略的,只不过加上比较好,相当于一个良好的习惯)

知道了上面的东西后,举个例子:

多返回值(以下三种写法本质上是一样的)

func div1(a, b int) (q, r int) {
    return a / b, a % b
}
func div2(a, b int) (int, int) {
    return a / b, a % b
}
func div3(a, b int) (q, r int) {
    q = a / b
    r = a % b
    return
}

注:第三种写法在函数很长的时候往往会让人不清楚是在哪里赋的值,所以不推荐使用

可变参数列表

注:go 语言没有函数重载,但有可变参数,只需在参数类型的前面加上三个点...即可

例如下面的求和函数,传多少个参数都可以

func sum(numbers ...int) int {
    s := 0
    for i := range numbers {
        s += numbers[i]
    }
    return s
}

函数式编程

函数式编程即第一个参数是回调函数,后面两个参数是传给回调函数的

举个例子

注:reflect 包里有和反射相关的东西,reflect.ValueOf(op).Pointer() 可以拿到 op 函数的指针

func apply(op func(int, int) int, a, b int) int {
    p := reflect.ValueOf(op).Pointer() // 获取回调函数的名字
    opName := runtime.FuncForPC(p).Name()
    fmt.Printf("回调函数%s的参数为(%d,%d)\n", opName, a, b)
    return op(a, b)
}

第一个参数为op func(int, int) int,后面的两个参数是a, b int,函数最后调用op(a, b)并返回了调用的结果。那这个函数从哪里来呢————我们自己写(有点像 Java 的匿名函数和 JavaScript 的箭头函数)。

例如,我们可以这样调用这个函数,最后打印的结果为 5

fmt.Println(apply(
    func(a, b int) int { // 匿名函数,如果有写好的函数可以直接放函数名在这里
        return a+b
    }, 2, 3))

数组

数组的定义

go 语言中,数组的定义也比较简单易懂

主要是有以下三种

var arr1 [5]int
arr2 := [3]int{1, 2, 3}
arr3 := [...]int{2, 4, 6, 8}

fmt.Println(
    arr1,
    arr2,
    arr3,
)

结果[0 0 0 0 0] [1 2 3] [2 4 6 8]

多维数组定义的语法也差不多

var grid [2][3]bool
fmt.Println(grid)

结果[[false false false] [false false false]]

注:数组也是值传递,函数内改变不影响外部

例:

func changeArr(arr [3]int) {
    arr[0] = 100
    fmt.Println("函数内", arr)
}
func main() {
    arr2 := [3]int{1, 2, 3}
    changeArr(arr2)
    fmt.Println("函数外", arr2)
}

结果 函数内 [100 2 3] 函数外 [1 2 3]即在函数内改变数组的值,函数外数组没变化(除非用指针)

pass:对于 go 语言的数组来说,[3]int[5]int 不是一种类型,所以如果运行changeArr(arr1)会报错(定义的var arr1 [5]int

数组的遍历

用普通的 for i 循环遍历

for i := 0; i < len(arr3); i++ {
    fmt.Println(arr3[i])
}

还可以用 range 关键字来遍历

遍历下标和值

for i, v := range arr3 {
    fmt.Println(i, v)
}

只要下标或值可以用下划线_替代不需要的元素

for _, v := range arr3 {
    fmt.Println(v)
}

大家可能发现了,数组给人的一种僵硬的感觉,因为数组的长度是固定的,想改变除非新建一个更大的数组然后把原数组放进去。这个在 go 语言中已经实现了,并且更加灵活可用,名为slice(切片)

切片的概念与创建

在讲完函数的基础之上,我们终于可以开始学习切片了。由于数组的长度是固定的,想改变除非新建一个更大的数组然后把原数组放进去。所以 go 语言为了更加方便就实现了,并且更加灵活可用,也就是名为 slice(切片)的东西。

切片概念

切片(slice)是一个视图,它的底层是一个数组

slice指针(ptr)长度(len)容量(cap) 组成

  • 指针(ptr) 指向 slice 第一个元素对应的底层数组元素的地址
  • 长度(len) 是 slice 的元素个数
  • 容量(cap) 是从 slice 的第一个元素到底层数组的最后一个元素的个数

slice 可以向后扩展,但是不可以向前扩展

s[i]不可以超越 len(s),向后扩展不可以超越底层数组 cap(s)

三者的联系之后会讲,今天讲讲创建

切片的创建

数组在 Go 中是固定长度且不可变的,定义数组使用 [ ] 后面跟上类型,再在后面使用一对大括号指定数组初始值

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

如果要定义可变长度的数组,可以使用 slice 类型和 make() 函数

我们先定义一个函数用来打印 slice 的长度、容量以及内部的元素

func printSlice(s []int) {
   fmt.Printf("len=%d cap=%d %v \n", len(s), cap(s), s)
}

直接定义

创建 slice 可以直接用var 变量名 变量类型来创建,只不过与数组不同,slice 的方括号内不用指定长度

var s []int
fmt.Println(s == nil)

注:切片的零值是 nil,所以上述代码会打印 true

使用字面值

与数组字面值不同的是,slice 的方括号里还是不用指定长度

s1 := []int{2, 4, 6, 8}
printSlice(s1)

结果 len=4 cap=4 [2 4 6 8]

使用 make() 函数

创建容量为 16 的切片可以用 make

s2 := make([]int, 16)
printSlice(s2)

结果len=16 cap=16 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

make 创建切片时,还可以分别指定长度和容量

s3 := make([]int, 10, 32)
printSlice(s3)

结果 len=10 cap=32 [0 0 0 0 0 0 0 0 0 0]

这里会发现 len 和 cap 并不相等,前者是内部元素的个数,后者是底层数组的总元素个数,为什么区分这个后面再说

还有一种方法:

我们先定义一个数组

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

定义一个切片,取出数组的第 3 个到第 6 个元素也就是下标为 2 到 5 的元素,区间是左闭右开的,所以是 2 到 6。数学的表达方式是 [2, 6)

s := arr[2:6]

是不是有那么点切片的感觉了

切片的操作

基本操作

定义一个数组

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

定义一个切片,取出数组的第 3 个到第 6 个元素也就是下标为 2 到 5 的元素,区间是左闭右开的,所以是 2 到 6,也就是数学上的[2,6)

s := arr[2:6]
fmt.Println("arr[2:6]", s)

结果 [2 3 4 5]

还有以下写法

fmt.Println("arr[:6] =", arr[:6]) // [0 1 2 3 4 5]
fmt.Println("arr[2:] =", arr[2:]) // [2 3 4 5 6 7]
fmt.Println("arr[:] =", arr[:])   // [0 1 2 3 4 5 6 7]

切片(slice)是一个视图,它的底层是一个数组,所以可以通过数组指针来修改底层数组的值

我们创建个函数来举个例子,函数的功能是修改传进来的 slice 的第一个元素为 100

func updateslice(s []int) {
   s[0] = 100
}
fmt.Println("s修改前", s) // [2 3 4 5]
updateslice(s)
fmt.Println("s修改后", s)     // [100 3 4 5]
fmt.Println("此时的arr", arr) // [0 1 100 3 4 5 6 7]

根据打印的结果来看,能成功修改,并且底层数组的值也被成功修改了

slice 扩展

arr1 := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr1[2:6]
s2 := s1[3:5]
fmt.Println("arr1", arr1) // [0 1 2 3 4 5 6 7]
fmt.Println("s1", s1)     // [2 3 4 5]
fmt.Println("s2", s2)     // [5,6] 也就是s1[3],s1[4]

但如果直接取 s1[4]则会报错,例如 fmt.Println(s1[4]) // 报错

此处我们可以发现,s2 将 s1 没有的元素也打印出来了,但是再往后就打印不了,我们看一张图或许就可以明白

注:图中的数值是索引,灰色的那块是 slice 的 cap

slices.png

我们可以打印结果来验证一下

fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n",
   s1, len(s1), cap(s1)) // s1=[2 3 4 5],len(s1)=4,cap(s1)=6
fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",
   s2, len(s2), cap(s2)) // s2=[5 6],len(s2)=2,cap(s2)=3

还记得上面的内容讲过:

  1. slice 由指针(ptr)长度(len)容量(cap) 组成
  2. 指针(ptr) 指向第一个 slice 元素对应的底层数组元素的地址
  3. 长度(len) 是 slice 的元素个数
  4. 容量(cap) 是从 slice 的第一个元素到底层数组的最后一个元素的个数
  5. slice 可以向后扩展,但是不可以向前扩展
  6. s[ i ] 不可以超越len(s),向后扩展不可以超越底层数组cap(s)

现在应该能明白点了(终于把上面挖的坑埋了)

向 slice 添加元素

  1. 向 slice 添加元素是不会改变 slice 的底层数组的,而是会重新分配一个数组
s3 := append(s2, 10)
s4 := append(s3, 11)
s5 := append(s4, 12)
fmt.Println("s3,s4,s5", s3, s4, s5) // [5 6 10] [5 6 10 11] [5 6 10 11 12]
fmt.Println("arr1", arr1)           // [0 1 2 3 4 5 6 10]
  1. 添加元素时如果超越 cap,系统会重新分配更大的底层数组,原来的数组如果没有被引用则会被垃圾回收
  2. 由于值传递的关系,必须接收 append 的返回值 一般来说,常用原来的 slice 变量接收 append 的返回值 s = append(s, val)

切片的复制

还是创建一个函数用来打印 slice 的长度、容量以及内部的元素

func printSlice(s []int) {
   fmt.Printf("len=%d cap=%d %v \n", len(s), cap(s), s)
}

然后我们创建几个 slice,便于后续的操作

s1 := []int{2, 4, 6, 8} // len=4 cap=4 [2 4 6 8]

s2 := make([]int, 16) // len=16 cap=16 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

s3 := make([]int, 10, 32) // len=10 cap=32 [0 0 0 0 0 0 0 0 0 0]

复制:

copy(s2, s1)
printSlice(s2)

结果 len=16 cap=16 [2 4 6 8 0 0 0 0 0 0 0 0 0 0 0 0]

切片的删除

删除下标为 3 的元素,可以用 append 在 s2[:3] 后面追加 s2[4:]...

s2 = append(s2[:3], s2[4:]...)
printSlice(s2)

结果 len=15 cap=16 [2 4 6 0 0 0 0 0 0 0 0 0 0 0 0]

切片的弹出(本质上和删除一样)

弹出首位元素

front := s2[0]
s2 = s2[1:]
fmt.Println(front) // 2
printSlice(s2)

结果len=14 cap=15 [4 6 0 0 0 0 0 0 0 0 0 0 0 0]

弹出最后一位元素

tail := s2[len(s2)-1]
s2 = s2[:len(s2)-1]
fmt.Println(tail) // 0
printSlice(s2)

结果 len=13 cap=15 [4 6 0 0 0 0 0 0 0 0 0 0 0]

Map

在 Go 里,定义键值对类型的结构也是使用叫做 map 的类型。可以使用 map 关键字进行定义,map 后面的中括号指定 key 的类型,中括号后面指定值的类型,再使用一对大括号指定初始值

map 的创建

主要有以下几种

m := map[string]string{
    "name":   "anna",
    "course": "golang",
}

m2 := make(map[string]int) // m2 == empty map

m3 := map[string]int{}     // m3 == empty map

var m4 map[string]int      // m4 == nil

fmt.Println(m, m2, m3, m4)
fmt.Println(len(m), len(m2), len(m3), len(m4))

注:为了便于代码的简介,此处创建的变量也可用于本篇文章后续的操作

结果:

map[course:golang name:anna] map[] map[] map[]
2 0 0 0

map 的遍历

注:遍历的顺序是随机的

for k, v := range m {
    fmt.Println(k, v)
}

结果可能每次的顺序都不一样

map 的取值

courseName := m["course"]
fmt.Println(courseName) // golang
causeName := m["cause"]
fmt.Println(causeName) // ""

最后一行代码由于 key 不存在,获得 Value 类型的初始值(string 初始值为空字符串)

还可以用 ok 来判断 key 是否存在

course, ok := m["course"]
fmt.Println(course, ok) // golang true
cause, ok := m["cause"]
fmt.Println(cause, ok)  // "" false
if ccc, ok := m["cause"]; ok {
    fmt.Println(ccc)
} else {
    fmt.Println("key does not exist")
}

注:空串在控制台中直接是什么都没有(包括双引号也没有,除非用特殊格式显示字符串的双引号)

map 的删除

fmt.Println("Deleting values")
name, ok := m["name"]
fmt.Println(name, ok) // anna true
delete(m, "name")
name, ok = m["name"]
fmt.Println(name, ok) // "" false

关于 map 的 key

  1. map 使用哈希表,必须可以比较相等
  2. 除了 slice,map,function 的内建类型都可以作为 key
  3. struct 类型不包含上述字段,也可以作为 key (编译时会检查)

字符和字符串处理

我们知道在其他语言中,string 表示字符串,char 代表字符,而 go 语言则有些不同

go 语言中 rune 相当于其他语言的 char

注:rune 是 int32 的别名

接下来看代码(与上文无关,上面相当于开场白)

s := "abc我爱学习"
fmt.Println(len(s))

运行后会发现,结果居然是 15 !!!

但是字符串内中英文加起来总共就 7 个字符才对,那么问题出在哪呢

UTF-8 是一种变长的编码方式,一个字符可能占用 1-4 个字节,英文字符占用 1 个字节,中文字符占用 3 个字节

我们将这个字符串转换为 byte 数组然后以二进制打印,看看结果

%X 表示以 16 进制

for _, b := range []byte(s) {
   fmt.Printf("%X ", b)
}

结果是 61 62 63 E6 88 91 E7 88 B1 E5 AD A6 E4 B9 A0

我们再打印一次,这次打印索引、十六进制、字符

for i, ch := range s {
   fmt.Printf("(%d %X %c) ", i, ch, ch)
}

这次的结果是 (0 61 a) (1 62 b) (2 63 c) (3 6211 我) (6 7231 爱) (9 5B66 学) (12 4E60 习) 索引从 0 1 2 3 直接跳到了 6 9 12,正好对应了“UTF-8 的中文字符占用 3 个字节

这样就有问题了,如果字符串比较长,而且混杂着中英文,那我们读取其中的某个索引的字符时就可能出现只读了这个字符的某个十六进制编码,从而只能得到乱码

由于各种中英文 UTF-8 编码占用字节数不同,所以无法直接通过索引访问字符串中的字符,而且效率低下

为了解决这个问题并且拥有更好的性能,rune 出现了。上述问题比较好的方法是将字符串转换为 rune 切片

for i, ch := range []rune(s) {
   fmt.Printf("(%d %c) ", i, ch)
}

结果 (0 a) (1 b) (2 c) (3 我) (4 爱) (5 学) (6 习) 是不是符合我们的需求了

对于字符串,最后我再给个小提示

tips: 一般对字符串处理时,先去看下 strings 包里有没有这个功能,没有再自己写

结构体

结构体入门

首先得知道

go 语言仅支持封装,不支持继承和多态
go 语言没有 class,只有 struct

定义语法

type 结构名 struct {
    属性名     类型
    属性名     类型
}

例如,定义一个坐标:

type Point struct {
    X int
    Y int
}

初始化结构可以使用结构名字,后面跟上一对大括号,里边给每个属性都赋值

func main() {
    p := Point{1, 2}
    fmt.Println(p) // {1, 2}
}

要访问或修改结构中的值,可以使用 . 点号语法

go 语言无论地址还是结构本身都是值传递,一律使用 . 来访问成员

例如:

p.X = 3
fmt.Println(p) // {3 2}

工厂函数

类似于其他语言的构造函数(记忆中久远的知识突然来攻击你)

go 语言中没有构造函数,但可以使用工厂函数

先定义一个 Node 结构,这个结构本身有一个 value,然后有左右两个节点(同样为 node)

type Node struct {
   Value       int
   Left, Right *Node
}

再来定义一个函数便于我们创建 Node,传入 value 就可以返回一个构建好的结构体

func CreateNode(value int) *Node {
   return &Node{Value: value}
}

可能有同学发现了,这个函数返回的是局部变量地址。返回局部变量的地址在 c++是错误的,而 go 中是安全可用的。

那问题来了,既然可以返回局部变量的地址,那么结构创建在堆上还是在栈上呢?

  • C++:局部变量是分配在栈上的,退出函数后会被回收,如果要传出去就需要分配在堆上,那就得手动释放
  • 其他语言例如 java:几乎所有的都是分配在堆上的,但是 java 中有垃圾回收机制,所以不用手动释放
  • go:不需要知道变量是在堆上还是在栈上,go 会自动管理内存,只要不再使用就会被回收

go 语言的编译器和运行环境会决定在堆还是在栈,所以我们不需要知道(发现 go 的强大之处了没)

结构体基础

上篇笔记我们讲了入门的一些语法,今天来更进一步

还是先定义一个 Node 结构,这个结构本身有一个 value,然后有左右两个节点(同样为 node)

type Node struct {
   Value       int
   Left, Right *Node
}

定义函数便于我们创建 Node,传入 value 就可以返回一个构建好的结构体

func CreateNode(value int) *Node {
   return &Node{Value: value}
}

为结构定义方法

我们来定义一个打印函数,也就是运行node.Print()时可以打印出各个节点的值

func (node Node) Print() {
   fmt.Println(node.Value)
}

这里和函数定义类似但略有不同,在函数名的前面多了个(自定义结构名 结构类型名)

函数名前面的 (node Node) 代表接收者,调用时用 . 来访问

func (node Node) Print() 和 func Print(node Node) 其实效果是一样的,只不过调用的方法不一样而已

前者 node.Print() 后者 Print(node)

一般来说,接收者是结构的一个副本,而不是结构本身,所以在方法中修改接收者的成员不会影响到结构本身(和普通函数一样)

主函数:

var root Node
fmt.Println(root) // {0 <nil> <nil>}
root = Node{Value: 3}
root.Left = &Node{} // 直接写Node{}和var root Node是一样的
root.Right = &Node{5, nil, nil}
root.Right.Left = new(Node) // new返回的是指针
root.Left.Right = CreateNode(2)

题外话:在 slice 中还可以直接写大括号

nodes := []Node{
   {Value: 3},
   {},
   {6, nil, nil},
}
fmt.Println(nodes)

结果 [{3 <nil> <nil>} {0 <nil> <nil>} {6 <nil> <nil>}]


建立好之后的 root 长这样

image.png

值接收者和指针接收者

前置定义

为了方便后续代码,我们先定义一些结构以及结构的函数(前面的笔记有说明)

先定义一个 Node 结构,这个结构本身有一个 value,然后有左右两个节点(同样为 node)

type Node struct {
   Value       int
   Left, Right *Node
}

主函数:

var root Node
fmt.Println(root) // {0 <nil> <nil>}
root = Node{Value: 3}
root.Left = &Node{}
root.Right = &Node{5, nil, nil}
root.Right.Left = new(Node) // new返回的是指针
root.Left.Right = Node{Value: 2}

建立好之后的 root 长这样

image.png

值接收者和指针接收者

如果我们想定义一个修改能 node 的节点的值的函数,那么需要传入指针,调用还是一样的。例如node.SetValue2(999)

func (node *Node) SetValue2(value int) {
   if node == nil {
      fmt.Println("Setting Value to nil node. Ignored.")
      return
   }
   node.Value = value
}

因为 node 本来就是指针,此时接收者为指针接收者,所以可以直接用 . 来调用而不是加&号取地址调用。

此代码判断 if == nil 是因为 go 语言的 nil 指针也可以调用方法,其他语言的 null 指针是不能调用方法的。好处是可以用 if node == nil { return } 来避免空指针异常

一般来说,接收者是结构的一个副本,而不是结构本身,所以在方法中修改接收者的成员不会影响到结构本身(和普通函数一样)

编译器很智能,要指针传指针,要值则从指针取值

注:go 语言中没有 this 指针,接收者的名字是自己显示定义的

值/指针接收者都可以接收值/指针,会自动转换

值接收者和指针接收者 tips:

  1. 要改变内容必须使用指针接收者
  2. 结构过大也考虑使用指针接收者,避免在每个方法调用中拷贝
  3. 一致性:如有指针接收者,最好都是指针接收者,便于维护

包与封装以及 gomod 的使用

包与封装

对于 go 语言来说,每个目录都是一个包,包名一般与目录名一致(可以不一样),而 main 包内包含可执行入口

封装的规范:包内结构体、函数名等一般使用CamelCase(即“骆驼拼写法”),名字首字母大写表示public,小写表示private(没错,和 java 中的 public、private 差不多)

注:为结构定义的方法必须放在同一个包内(可以是不同文件),不能跨包调用,但是可以跨包访问

命名规范:结构名命名时不需要重复包名,例如 treeNode 在 tree 包内可以直接叫 Node,不需要重复 tree 这个字段了,不然在其他包内调用就得用tree.treeNode感觉很不好看,tree.Node则优雅了许多

gomod 的使用

命令

清除无用的依赖: go mod tidy
查看依赖图: go mod graph
初始化依赖(创建 go.mod 文件): go mod init 自定义的 mod 名
编译当前目录下的所有包: go build ./... 或者 go build
更新依赖: go get -u github.com/xxx/xxx

一些第三方库

功能强大的日志包: go get -u go.uber.org/zap
一个经典的 http 框架: go get -u github.com/gin-gonic/gin
Go 操作 mysql 数据库的标准库之一: go get -u github.com/go-sql-driver/mysql
Go 的 ORM: go get -u github.com/jinzhu/gorm
计划任务库: go get -u github.com/robfig/cron/v3

拓展已有类型

前言

拓展已有类型(包括系统类型和别人的类型)的方式有三种:1.定义别名、2.使用组合、3.使用内嵌(Embedding)

区别:

  1. 定义别名:只是给一个类型起了一个新的名字,本质还是原来的类型(最简单的方法)
  2. 使用组合:在一个结构里面声明另一个匿名结构,本质还是原来的类型(最常用的方法)
  3. 使用内嵌(Embedding):在一个结构里面声明另一个匿名结构,本质还是原来的类型
 但是可以直接使用匿名结构的方法,而不需要通过匿名结构来调用
 (可以节省许多代码,但是除非维护代码的人收悉go,否则不简易,除非真的省下了许多代码)

原版类型

type Node struct {
   Value       int
   Left, Right *Node
}

打印函数

func (node Node) Print() {
   fmt.Println(node.Value)
}

定义别名

此处封装的分别是 slice 中的添加元素、切片弹出和判断是否为空的功能

type Queue []int

func (q *Queue) Push(v int) {
   *q = append(*q, v)
}
func (q *Queue) Pop() int {
   head := (*q)[0]
   *q = (*q)[1:]
   return head
}
func (q *Queue) IsEmpty() bool {
   return len(*q) == 0
}

使用组合

type myTreeNode struct {
   node *tree.Node
}

后序遍历

后序遍历即为 先遍历左 再遍历右 最后遍历根 (也就是左右中)

func (myNode *myTreeNode) postOrder() {
   if myNode == nil || myNode.node == nil {
      return
   }

   left := myTreeNode{myNode.node.Left}
   right := myTreeNode{myNode.node.Right}

   left.postOrder()
   right.postOrder()
   myNode.node.Print()
}

使用内嵌

type myTreeNode2 struct {
   *tree.Node // 使用内嵌(Embedding),"."后面的Node就是tree.Node的别名
}

内嵌版后序遍历

内嵌的好处是可以用 myNode.Node,也可以直接用 myNode,相当于一个语法糖

func (myNode *myTreeNode2) postOrder() {
   if myNode == nil || myNode.Node == nil {
      return
   }

   left := myTreeNode2{myNode.Left} // 主要是这里有点不一样
   right := myTreeNode2{myNode.Right} // 主要是这里有点不一样

   left.postOrder()
   right.postOrder()
   myNode.Node.Print()
}

以上就是我对 Go 语言基础语法入门的一些理解,可能不太全面,也可能会有些错误,希望大家能够指出并多多包涵,我会尽快修正,谢谢