Go 语言语法学习
最近工作有些繁重,没有实现每日一更,有些遗憾。不过好在没有停止学习,这段时间把 Go 语言的语法撸了一遍,今天来总结下。 我的笔记是基于我的知识体系来查漏补缺,不适合每一个人,我的编程经验限于 C 语言和 Python 语言。
前面几篇学习笔记里已经记录了 Go 语言的标识符、关键字、数据类型,了解了 Go 语言变量、常量的声明与使用,今天继续总结写其他的方面。
运算符
算术运算符
加减乘除(+ - * /)、自增(++)、自减(--)、取余(%)。
关系运算符
关系运算符的表达式结果为布尔类型(True 或 False)。
关系运算符有 ==、!=、>、>=、<、<=。
逻辑运算符
逻辑运算符的表达式结果为布尔类型(True 或 False)。
逻辑运算符有逻辑与(&&)、逻辑或(||)、逻辑非(!)。
布尔运算符
布尔运算符或位运算符是按照表达式两侧每一位进行布尔运算得到的结果。
布尔运算符有与(&)、或(|)、异或(^)。
其它位运算符有左移(<<)、右移(>>)。Go 语言左移运算是将高位丢弃,低位补 0;右移舍弃最低位,最高位补符号位。
条件语句与循环语句
if 条件语句
import "fmt"
var score int = 80
if score >= 80 {
fmt.Println("优")
} else if score >= 60 {
fmt.Println("良")
} else {
fmt.Println("差")
}
switch 条件语句
switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break。switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
。使用 fallthrough
会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
import "fmt"
var score int = 80
switch {
case score >= 80:
fmt.Println("优")
case score >= 60:
fmt.Println("良")
default:
fmt.Println("差")
}
select
严格上说,select 不能是条件语句,它更像一种通信方式,类似 POSIX 中的 POLL/SELECT 机制。通过 select 同时监听多个事件源(channel),有任意一个事件到就执行,否则就阻塞。
循环语句
在 Go 语言中循环语句只有 for,使用方式与 C 语言类似。
for init; condition; post { } /* 和 C 语言的 for 一样 */
for condition { } /* 和 C 的 while 一样 */
for { } /* 和 C 的 for(;;) 一样 */
for 循环也经常用于遍历数组、切片、map,通常与 range 配合使用,应用如下:
for key, value := range oldMap {
newMap[key] = value
}
循环语句中还涉及到 break
、continue
、goto
关键字用于程序跳转,这些关键字的使用与 C 语言无异。
关于 goto
语句,很多人说会打乱程序结构,不建议使用。但是,在系统程序设计的时候,它会是一个非常有用的特性,会使得程序逻辑更加清晰。
函数
函数是最基本的代码抽象,将多个相同功能的语句封装成一个函数来对外提供一个功能。Go 语言中至少有一个 main 函数,作为整个程序的入口(main 函数执行之前还有 init 函数)。
函数定义
使用 func 关键字定义函数。
func function_name( [parameter list] ) [return_types] {
函数体
}
- function_name:函数名
- [parameter list]:是函数入参(形参),多个入参使用逗号隔开,注意入参不用使用
[]
包裹,这里的[]
指示这是一个可选项 - [return_types]:函数返回值,同样
[]
代表这是一个可选项,没有的话,函数就没有返回值
从函数体中返回可以使用 return
关键字,支持多返回值。
闭包
目前我还不知道为什么会有闭包,能给编程带来哪些不一样?
闭包在 Go 语言中作为匿名函数,是一个内联语句或表达式。匿名函数相对普通函数而言,其没有函数名,但可以有入参和返回值。闭包包含在原有的函数声明里,可以在闭包(匿名函数)中直接使用原有函数内的变量。
引用菜鸟教程上的示例:
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
方法
Go 语言中的方法其实是一种面向对象的封装,与 C++ 的 class 类似,只是 Go 中的应用在定义类中的方法和成员变量的时候是分离的。
其定义的语法格式为:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
可以理解为给 variable_data_type
数据类型扩充了一个方法 function_name
,该方法的返回值类型是 return_type
。
引用菜鸟教程上的示例:
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
上述代码中定义了一个 Circle
结构体,该结构体包含一个 radius
成员变量。然后使用 func (c Circle) getArea() float64
为 Circle
结构体扩展了一个 getArea
方法,用于计算圆的面积,此时 Circle
就相当于一个类。var c1 Circle
相当于声明了一个类实例 c1,可以通过 c1 来引用 radius 成员变量和 getArea 方法。
变量作用域
变量的作用于限制了变量在代码中可使用的位置,Go 语言的变量作用域与 C 语言一致。
全局变量
定义在函数外,全局变量可以在整个包中引用,导出后可以在外部包引用(导出类似 C 语言的 extern 关键字)。
局部变量
定义在函数内(函数形参也是局部变量,函数返回值也是局部变量),使用范围在函数体内。
数组
数组是同一数据类型的数据集合,存储在连续的内存中。
定义数组需要使用 []
:
var variable_name [SIZE] variable_type
如上代码所示,定义了数组 variable_name
,数组中的数据的数据类型是 variable_type
,数组的大小是 SIZE
。
数组的读写是通过 下标 的方式,从 0 开始,范围是 [0, SIZE-1]
。
多维数组的定义如下所示:
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
指针
指针也是一种数据类型,声明一个指针类型变量需要使用 *
号,如下所示:
var a int = 10
var ip *int /* 声明指针变量 */
ip = &a
var b int = *ip
如上代码所示,声明了一个指向 int 类型数据的指针 ip
,指针变量 ip 里存的值是 int 类型数据的起始地址。通过 &
符号取变量 a 的地址,并赋值给 ip。要获取 ip 指针指向的值,需要使用 *
号。
空指针为 nil
,指代 0 值或空值。
提到指针就会想到 C 语言中指针的广泛用途,更有指针的指针(双重指针)、指针数组、数组指针等,这里不做扩展。
结构体
结构体是对一个事物所有属性的抽象,是一个事物所有属性的集合。
比如一本书有标题、作者、出版社、图书编号等属性,将这些数据组合到一起,就是 书 的数据属性。
结构体定义如下:
type struct_variable_type struct {
member1 data_type;
member2 data_type;
...
membern data_type;
}
如上就定义了一个 struct_variable_type
结构体数据类型,然后可以使用该类型进行声明一个变量:
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
如果要访问结构体成员,需要使用点号 .
操作符,格式为:
结构体.成员名"
`
切片 slice
Go 语言的切片跟 Python 的列表(list)很像。
Go 语言的切片是对数组的抽象,为了解决变长数组的场景。Go 数组的长度是固定的,切片的长度是可以通过通过追加元素变大的。
切片的定义与声明:
var identifier [] type // 通过空数组的方式声明一个切片
var slice1 []type = make([]type, len, capacity) // 通过 make() 函数定义一个切片,len 是切片的初始长度,capacity 是可选参数,表示切片的容量
slice1 := make([]type, len)
切片支持使用 len() 方法和 cap() 方法来分别获取切片的长度和容量。
空切片的值为 nil。
切片可以使用 append(slice, v1, v2...) 函数来追加数据。
切片使用 copy(new_slice, old_slice) 来拷贝数据。
范围 range
Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对的 key 值。
Go range 也可以用于枚举字符串,返回的第一个参数是 Unicode 字符的索引,第二个是 Unicode 字符的值。
集合 map
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对。
Go 的 map 就是 Python 的字典类型。
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
Go map 删除元素使用 delete(map_var, key) 方法。
通过对 range、数组、map、slice 的学习,真切感觉到 Go 语言并不是为了做一个好的语言(与 rust 语言的发展路线不同),而是为了解决实际的问题。Go 语言的这些高级数据类型没法跟 Python 比。
类型转换
使用新的数据类型包裹原数据即可,称为显示转换,如下:
var a int = 1 // a 是 int 数据类型
var b float32 = float32(a) // 将 a 转换为 float32 数据类型,并赋值给 b
接口 interface
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
Go interface 数据类型与 Go 函数中的方法的定义差不多,也是对 class 的一个封装。但,Go 的 interface 在使用过程中非常别扭。
错误处理
Go 语言提供了 error 接口(interface)用于错误处理机制。通常我们可以使用 errors.New("error msg") 返回一个错误信息。当然我们也可以重新实现 error 接口中的 Error 方法,如下:
error 的原型为:
type error interface {
Error() string
}
我们定义一个 Div0Error 结构体,并实现 error 接口:
type Div0Error struct {
dividee int
divider int
}
func (div *Div0Error) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
演示:
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := Div0Error{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当被除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
并发
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
go 函数名( 参数列表 )
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
goroutine 相当于进程中起的线程,所有线程共享内存。
通道 channel
既然已经有了线程的概念,那么就会存在线程间的同步和通讯问题,Go 使用通道(channel)来实现。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。使用操作符 <-
,符号左边是接收者,右边是发送者。
使用 make 创建 channel,如下:
ch := make(chan int, 100) // make 第二个参数 100 是该通道的缓冲区,是一个可选参数,如果不指定,那么就是无缓冲的通道
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
通道与消息队列是等效的,如果通道缓冲区满,那么再往通道里塞数据,就会阻塞该 goroutine;同样,如果通道缓冲区没有数据了,再次接收通道数据,也会阻塞该 goroutine。
最后
昨天,也就是 2019-09-30,出于工作需求,需要一个外网的 UDP 服务器,所以就尝试用 Go 语言来部署了一个应用实例,学以致用,小开心。
通过部署,发现还有很多知识需要补充。比如 Linux 系统应用与运维,服务器的安全策略等,这些知识技能都需要我后面学习。
参考
本文中部分内容摘自菜鸟教程,侵删。
有疑问加站长微信联系(非本文作者)
