Shudan Wang

没有人不爱惜他的生命,但很少人珍视他的时间。

  • 主页
  • 随笔
所有文章 友链 关于我

Shudan Wang

没有人不爱惜他的生命,但很少人珍视他的时间。

  • 主页
  • 随笔

《Go语言核心36讲》之“Go语言基本语法”

2020-08-17

Go 语言的基本语法在很多特性上和C语言非常相近。本节我们就学习下Go 语言的变量、常量、数据类型、运算符以及流程控制的相关知识点。
Go 语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,让 Go 语言能够推导出它们的类型(自动类型推断)。

Go 语言变量

Go 语言声明变量的一般形式为var identifier type,type类型可以是其预定义的基本类型,也可以是程序自定义的函数、结构体或接口。声明变量的方式有:

1
2
3
4
5
6
7
1) var name string                       // 系统自动赋予它该类型的零值,比如string ""  
var firstName, lastName string // 声明多个变量同类型变量

2) var name = "Robert" // 定义并初始化变量,可省略类型
var name, age = "Robert", 25 // 定义并初始化多个变量,类型可相同/不同,自动推断类型

3) name := "Robert" // 简短变量定义,自动推断类型

所谓“类型推断”,就是一种编程语言在编译期自动解释表达式类型的能力,它只能用于变量或常量的初始化。Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率(这种类型确定是在编译期完成)。
特别说明的是,简短变量定义只能在函数体内部使用(局部变量),比如在编写if、for、switch语句时,经常把它按插在初始化子句中,用来声明一些临时的变量。var 形式的声明语句则没有啥子要求。
Go 语言还具有 “多重赋值”特性,可以轻松完成变量交换的任务。多重赋值在Go语言的错误处理和函数返回值中会大量地使用。

1
2
3
4
5
var a, b = 100, 200

a, b = b, a // 多重赋值时,变量类型必须相同,并按从左到右顺序赋值

fmt.Println(a, b)

Go 语言还有一种特殊变量——“匿名变量”,它以’_’为标志符,可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃。它不占用内存空间,也不会分配内存,所以匿名变量与匿名变量之间也不会因为多次声明而无法使用。

关于 “Go 语言变量的作用域”(局部变量、全局变量、形式参数),静态语言一贯尿性,不再赘述。

Go 语言常量

Go 语言常量是程序编译阶段就确定的值,在程序运行过程中无法被修改。它的数据类型只能是其预定义的基本类型,声明方式也更简单一些:

1
2
3
4
5
6
const a = 100

const (
a = 100
b = 200
)

Go 语言常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,而不用每行都写一遍初始化表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义连续常量或位常量等,类似枚举类型
const (
Monday = iota + 1 # 枚举值:1, 2, 3,...
Tuesday
Wednesday
Thursday
Friday
)

const (
Open = 1 << iota # 枚举值:1, 2, 4
Close
Pending
)

Go 语言常量还有个不同寻常之处——无类型常量,编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit 的运算精度。它们可以直接用于表达式而不需要显式的类型转换:

1
2
3
4
5
6
7
# 有 六种 未明确类型的常量类型:无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串

math.MaxFloat32 // 表示 float32 能取到的最大数值,大约是 3.4e38;
math.MaxFloat64 // 表示 float64 能取到的最大数值,大约是 1.8e308;

var x float32 = math.Pi
var y float64 = math.Pi

Go 语言数据类型

Go 语言的基本数据类型有:

  • 布尔类型(bool);
  • 有符号整数类型 int(32 或 64位)、int8、int16、int32、int64;
  • 无符号整数类型 uint(32bit 或 64bit)、uint8、uint16、uint32、uint64、uintptr(不指定具体的bit但足以容纳指针值);
  • 字节类型(byte),uint8的别名,一般用于强调数值是一个原始数据而不是一个小整数;
  • rune 类型,int32 的别名,通常用于表示一个 Unicode 码点;
  • 浮点类型 float32、float64;
  • 复数类型 complex64、complex128;
  • 字符串类型(string),只读的byte slice,Go 语言内部使用 UTF-8 编码标识 Unicode 文本,通过 rune 类型可方便地对每个 UTF-8 字符进行访问。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var s = "中华人民共和国"

    r := []rune(s) // rune(unicode) 类型切片
    fmt.Println(len(r)) // 7
    fmt.Printf("华的unicode %x\n", r[1]) // 华的unicode 534e

    for _, c := range s{ // 注意string类型range循环输出的是rune,而不是byte
    fmt.Printf("%[1]c %[1]x\n", c) // %[1]c表示第一个参数以%c格式化输出
    }

除此之外,Go 语言还有一些派生类型,包括:指针类型(pointer)、数组类型、结构化类型(struct)、channel类型、函数类型、切片类型、接口类型(interface)、map类型。
特别说明的是,Go 语言不允许隐式类型转换(比如某些主流语言默认允许将一个取值范围较小的类型转换到一个取值范围较大的类型【相同底层类型】),使用时必须显式的对类型进行转换(别名和原有类型也不能进行隐式类型转换)。

1
valueOfTypeB = typeB(valueOfTypeA)

Go 语言指针

从传统意义上说,指针是一个指向某个确切的内存地址的值,该内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。Go 语言中可以代表“指针”的常见类型有 Go内建的uintptr类型、标准库的unsafe.Pointer。
Go 语言的类型指针不能进行偏移和运算。
Go 语言中有些值是 不可寻址( & 引发编译错误),这些值会具有以下某个特性:不可变的值(比如常量、字符串变量的值、函数以及方法的字面量等)、临时结果的值(比如算术操作、针对值字面量的表达式结果值等)、不安全操作(取址可能会破坏程序的一致性,比如对字典的索引结果值的取址操作)。另外,我们也无法调用一个不可寻址值的指针方法。

1
2
3
4
5
6
7
8
9
10
11
- 常量的值。
- 基本类型值的字面量。
- 算术操作的结果值。
- 对各种字面量的索引表达式和切片表达式的结果值。不过有一个例外,对切片字面量的索引结果值却是可寻址的。
- 对字符串变量的索引表达式和切片表达式的结果值。
- 对字典变量的索引表达式的结果值。
- 函数字面量和方法字面量,以及对它们的调用表达式的结果值。
- 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。
- 类型转换表达式的结果值。
- 类型断言表达式的结果值。
- 接收表达式的结果值。

Go 语言数组

Go 语言数组同样是一个由固定长度的特定类型元素组成的序列,这种类型可以是任意的基础数据类型、或自定义类型。声明数组的方式有:

1
2
3
4
5
6
7
8
9
10
11
1) var arr [5] float32                                 // 默认情况下,数组的每个元素都会被初始化为元素类型对应的零值

2) var arr = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 声明并初始化数组
var arr = [5]float32{0:1000.0, 2:3.4} // 声明并初始化数组中指定元素,其余为元素类型的默认零值
var arr = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // Go 语言会根据元素的个数来设置数组的大小


//打印索引和元素
for i, v := range arr{
fmt.Printf("%d %d \n", i, v) // v是对应索引的值拷贝,具有只读性质
}

Go 语言中如果两个数组类型相同(包括数组的长度,数组中元素的类型),我们可以直接通过较运算符(==和!=)来判断它们是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的。
Go 语言中允许使用多维数组,常用的多维数组声明方式如下:

1
2
3
4
5
6
7
var arr [SIZE1][SIZE2]...[SIZEN] float32

//二维数组是最简单的多维数组,本质上是由一维数组组成的
1) var arr [4][2] int

2) var arr = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
var arr = [4][2]int{1: {20, 21}, 3: {40, 41}}

Go 语言还支持快速的 数组截取(结果为切片),arr[开始索引(包含), 结束索引(不包含)]。

Go 语言切片(Slice)

Go 语言切片默认指向一段连续的内存空间(切片的底层数组),使用上类似可变长数组,本质上是个如下图示的结构体,声明切片的方式有:

1
2
3
4
5
6
7
1) var s [] int                                 // 声明空(nil)切片 len(s) = cap(s) = 0

2) var s = make([]int, length, capacity) // 使用 make() 函数创建切片

3) var s = []int{1,2,3} // 声明并初始化切片 len(s) = cap(s) = 3

4) var s = arr[startIndex:endIndex] // 数组截取以创建新的分片,注意s是数组arr的引用

Go 语言的内建函数append()可以为切片动态追加元素,如果空间不足以容纳足够多的元素,切片就会进行 “扩容”(创建新的大小连续存储空间,并将原值COPY过去)。
切片容量的扩展规律默认是按容量的 2 倍数进行扩充(如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准),但当切片的长度 >=1024 时,Go 语言将按容量的 1.25 倍作为新扩充基准。最终,新容量往往会比新长度大一些,当然相等也是可能的。更多细节可参见runtime包中 slice.go 文件里的growslice()等相关函数的具体实现。

1
2
3
4
5
var s []int
for i:=0; i<10; i++{
s = append(s, i)
fmt.Printf("len: %d cap: %d pointer: %p\n", len(s), cap(s), s)
}

Go 语言的内置函数copy()可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

1
2
3
copy( destSlice, srcSlice []T) int

其中,目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数

切片具有共享存储结构的特性(包括数组截取和切片截取),我们来看个例子:

1
2
3
4
5
6
7
8
9
year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}

Q2 := year[3:6]
fmt.Println(Q2, len(Q2), cap(Q2)) // [Apr May Jun] 3 9

summer := year[5:8]
fmt.Println(summer, len(summer), cap(summer)) // [Jun Jul Aug] 3 7
summer[0] = "Unknown"
fmt.Print(Q2) // [Apr May Unknown]

上例的切片变量内存结构示意图如下,这部分大家容易在实际编程中犯错,使用时慎重(配合gc,及时将不再使用的slice置为nil)。

Go 语言Map

Go 语言Map 是一种无序的键值对集合,也称为关联数组或字典(底层使用hash表来实现)。声明 map 的方式有:

1
2
3
4
5
6
1) var mapLit map[string] int                      // 声明空(nil) map,cannot assignment
var mapLit = map[string] int{} // 初始化空map,can assignment

2) var mapLit = make(map[string] int [,cap]) // 使用 make() 函数创建map,可选性标明初始容量 capacity(不支持cap())

3) var mapLit = map[string] int{"one":1, "tow":2} // 声明并初始化 map

其中,map 的 key 类型是受限的,value 则可以是任意类型。key 类型宥于限制的根本原因在于哈希表的映射过程(键值 -> hash function哈希值 -> (低几位定位)哈希桶 -> 查找哈希值 + 判等键值)。

1
2
3
4
5
6
7
1. Go 语言键类型的值必须支持 判等 操作,所以不能是函数类型、字典类型和切片类型;
如果键类型是接口类型,那么键值的实际类型也不能是上述三种类型(引发panic);
如果键的类型 是数组类型,请确保该类型的元素类型不是函数类型、字典类型或切片类型

2. 求哈希和判等操作的速度越快,对应类型就越适合作为键类型;
优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束;
不建议使用派生(高级数据)类型作为字典的键类型,不仅仅是因为对它们的值求哈希,以及判等的速度较慢,更是因为在它们的值中存在变数;

当 value 类型为函数时,结合 Go 的 Dock type 接口方式,可以方便地实现单一方法对象的工程模式。

1
2
3
4
5
6
# 计算参数 N 次幂
m := map[int]func(op int) int{}
m[1] = func(op int) int{ return op}
m[2] = func(op int) int{ return op*op}
m[3] = func(op int) int{ return op*op*op}
fmt.Println(m[1](2), m[2](2), m[3](2)) // 输出 2, 4, 8

当访问Go 语言 map 中不存在的 key 时,它不会报错并直接返回值类型对应的零值。为了区分“访问的 key 不存在” 与 “m[key]=零值”的情况:

1
2
3
4
5
if v, ok := m[key];ok{
fmt.Println("m[key] = ", v)
}else{
fmt.Println("key is not existing")
}

Go 语言的内置函数delete(),可以删除容器中的元素。

1
2
3
4
5
6
7
scene := map[string]int{"route":66, "brazil":4, "china":960}

delete(scene, "brazil")

for k, v := range scene {
fmt.Println(k, v) // route 66 china 960
}

Go 语言的内置集合中没有 Set 实现,我们可以用map[type]bool模拟实现。

Go 语言运算符

Go 语言内置的运算符有:

  • 算术运算符:+、 -、 *、 /、 %、 ++、 –(Go语言没有前置的++、–);
  • 关系运算符:==、!=、 >、 <、 >=、 <=;
  • 逻辑运算符:&&、||、!;
  • 位运算符:&、|、^、 <<、 >>;

    Go 语言流程控制

    Go 语言中的基本流程控制语句,包括分支语句(if 和 switch)、循环(for、break 和 continue)和跳转(goto)语句。

    Go 语言条件语句

    Go 语言if条件语句与主流编程语言区别不大,主要差异在于:1. condition 表达式结果必须为布尔值;2. 在 if 表达式之前可以添加一个执行语句,并根据变量值进行判断if var declaration; condition {...}。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    if condition {
    // do something
    } else {
    // do something
    }

    if condition1 {
    // do something
    } else if condition2 {
    // do something else
    }else {
    // catch-all or default
    }

    # 将返回值与判断放在一行进行处理
    if err := Connect(); err != nil {
    fmt.Println(err)
    return
    }

Go 语言switch语句比主流编程语言更加通用,它的1) 条件表达式不限制为常量或者整数;2) 一分支多值——单个case 中可以出现多个(使用逗号分割)的结果选项;3) case 与 case 之间是独立的代码块而不需要通过break语句明确退出一个case(如果要[强制]执行[紧挨]后面的 case,可以使用 fallthrough);4) 可以不设定switch之后的条件表达式,此时整个switch结构与多个if…else…的逻辑作用等同。

1
2
3
4
5
6
7
8
9
10
# 使用 type-switch来判断某个 interface 变量中实际存储类型
switch x.(type){
case type:
statement(s);
case type:
statement(s);
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s);
}

特别说明的是,switch表达式的结果要与 case 表达式的所有子表达式的结果值做判等操作,所以它们的类型必须相同或者能够都统一到switch表达式的结果类型,否则会引发编译错误。另外,对于由字面量直接表示的子表达式而言,同一条switch语句中的所有case表达式的子表达式的结果值不能重复。

Go 语言循环语句

Go语言中的循环语句只支持 for 关键字,写法与C语言类似。

1
2
3
4
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}

总结

Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。当然,从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。

由于结构化类型(struct)、channel类型、函数类型、切片类型、接口类型(interface)的相关知识点较多,我们将在后续章节中继续学习。

赏

谢谢你请我吃糖果

支付宝
微信
  • GO

扫一扫,分享到微信

微信分享二维码
《Go语言核心36讲》之“使用函数的正确姿势”
《GO语言核心36讲》之“命令源码文件与库源码文件”
  1. 1. Go 语言变量
  2. 2. Go 语言常量
  3. 3. Go 语言数据类型
    1. 3.1. Go 语言指针
    2. 3.2. Go 语言数组
    3. 3.3. Go 语言切片(Slice)
    4. 3.4. Go 语言Map
  4. 4. Go 语言运算符
  5. 5. Go 语言流程控制
    1. 5.1. Go 语言条件语句
    2. 5.2. Go 语言循环语句
  6. 6. 总结
© 2023 Shudan Wang
Hexo Theme Yilia by Litten
  • 所有文章
  • 友链
  • 关于我

tag:

  • CI框架源码解析
  • HTTP原理
  • 网络技术
  • Laravel
  • Linux基础篇
  • Linux之常用命令
  • php杂集
  • 随笔
  • PHP
  • 网络安全
  • GO
  • mysql
  • Apache
  • Git
  • SSH
  • V2Ray
  • 数据结构
  • HTML
  • Nginx
  • NoSQL
  • Docker
  • Memcached

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 廖雪峰
  • 阮一峰
  • 陈皓
很惭愧

只做了一点微小的工作
谢谢大家