文章目录
- 前言
- Go词法单元
- token
- 标识符
- 关键字( 25个 )
- 内置数据类型标识符( 20个 )
- 内置函数( 15个 )
- 常量值标识符( 4个)
- 空白标识符( 1个 )
- 操作符和分隔符
- 字面常量
- 变量和常量
- 变量
- 常量
- 基本数据类型
- 布尔类型
- 整型
- 浮点型
- 复数类型
- 字符串
- rune类型
- 复合数据类型
- 指针
- 数组
- 数组初始化
- 数组的特点
- 数组相关操作
- 切片
- 切片的创建
- 切片支持的操作
- 字符串和切片的相关转换
- map
- map的创建
- map支持的操作
- struct
- struct类型字面量
- 自定义struct类型
- struct类型变量的初始化
- 其他复合类型
- 控制结构
- if 语句
- switch 语句
- for 语句
- 标签和跳转
- 标签
- goto
- break
- continue
- return 和函数调用
- 总结
前言
GO语言是由Google开发的一门开源编程语言,它的设计目标是简洁高效。GO语言作为一门静态类型、编译型语言,兼具了高效的执行速度和易于开发维护的特性。在本篇博客中,我们将深入探讨GO语言的基础语法,重点突出其独特之处以及如何利用这些特性编写优雅的代码。
Go词法单元
在介绍 Go 语言具体语法之前,先介绍一下现代高级语言的源程序内部的几个概念: token 、
关键字、标识符、操作符、 分隔符和字面量。这种语言层面通用的概念非常重要,这些概念能够帮助程序员更好地掌握语言的语法结构 。
token
token 是构成源程序的基本不可再分割的单元。编译器编译源程序的第一步就是将源程序分割为一个个独立的 token,这个过程就是词法分析。 Go 语言的 token 可以分为关键字、标识符 、操作符、分隔符和字面常量等。
Go 语言里面的 token 是怎么分割的昵? Go 的 token 分隔符有两类 : 一类是操作符,还有一类自身没有特殊含义,仅用来分隔其他 token ,被称为纯分隔符。
- 操作符: 操作符就是一个天然的分隔符,同时其自身也是一个 token, 语句如下所示 。
sum := a + b
“:=”和“+”既是分隔符,也是token,所有这个简单的语句被分割为5个token:“sum”、“:=”、“a”、“+”、“b”。
- 纯分隔符: 其本身不具备任何语法含义,只作为其他 token 的分割功能。包括空格、制表符、换行符和回车符 , 多个相邻的空格或者制表符会被编译器看作分隔符处理,比如如下语句
package main
这是一个包声明的语句, package 和 main 之间可以有任意多个空格或者制表符 , Go 编译器会将其作为一个分隔符处理,最后分离出来两个token:package 和 main 。
标识符
编程语言的标识符用来标识变量、类型 、 常量等语法对象的符号名称,其在语法分析时作为一个 token 存在 。编程语言的标识符总体上分为两类 : 一类是语言设计者预留的标识符, 一类是编程者可以自定义的标识符。 前者一般由语言设计者确定 , 包括语言的预声明标识符及用于后续言扩展的保留字;后者是用户在编程过程中自行定义的变量名、常量名、函数名等一切符合语言规范的标识符。
Go 的标识符构成规则是: 开头一个字符必须是字母或下划线,后面跟任意多个字符、数字或下划线 , 并且区分大小写, Unicode 字符也可以作为标识符的构成,但是一般不推荐这么使用。我们在定义新的标识符时要避开 Go 语言预声明标识符,以免引起混乱。
9aa //这不是一个合法标识符,不是以字母或下画线开头
-aa //这不是一个合法标识符,不是以字母或下画线开头
aa //这是一个合法标识符
_aa //这是一个合法标识符
aa911 //这是一个合法标识符
_aa911 //这是一个合法标识符
Go 语言预声明的标识符包括关键字、内置数据类型标识符、常量值标识符 、 内置函数和空白标识符。在写 Go 源程序的过程中,用户自定义标识符用在包名、函数名 、 自定义类型名、变量名和常量名等上。
关键字( 25个 )
编程语言里面的关键字是指语言设计者保留的有特定语法含义的标识符,这些关键宇有自己独特的用途和语法含义,它们一般用来控制程序结构,每个关键字都代表不同语义的语法糖 。Go 语言是一门极简的语言,只有如下 25 个关键字 :
break、default、func、interface、select
case、defer 、go、map、struct
chan、else、goto、package、switch
const、fallthrough、if、range、type
continue、for、import、return、var
内置数据类型标识符( 20个 )
丰富的内置类型支持是高级语言的基本特性,基本类型也是构造用户自定义类型的基础。为了标识每种内置数据类型,Go 定义了一套预声明标识符,这些标识符用在变量或常量声明时。Go 语言内置了 20 个预声明数据类型标识符。
- 数值( 16 个)
- 整型( 12 个)
byte int int8 int16 int32 int64
uint unint8 uint16 uint32 uint64 uintprt- 浮点型( 2 个)
float32 float64- 复数型( 2 个)
complex64 complex128- 字符和字符串型( 2 个)
string rune- 接口型( 1 个)
error- 布尔型( 1 个)
bool
Go 是一种强类型静态编译型语言,在定义变量和常量时需要显式地指出数据类型,当然Go 也支持自动类型推导,在声明初始化内置类型变量时, Go 可以自动地进行类型推导。但是在定义新类型或函数时,必须显式地带上类型标识符 。
内置函数( 15个 )
make new len cap append copy delete panic recover close complex real image
Print Println
内置函数也是高级语言的一种语法糖,由于其是语言内置的,不需要用 import 引入,内置
函数具有全局可见性 。 注意到其都是以小写字母开头的,但是并不影响其全局可用性 。
常量值标识符( 4个)
true false //true 和 false 表示 bool 类型的两常量值:具和假
iota //用在连续的枚举类型的声明中
nil //指针/引用型的变量的默认值就是 nil
Go 的常量值标识符代表的是一个常量值,这个常量值表达特殊的含义,不好使用常量字面量直接表述时,就使用 一个预先声明的标识符代替。
空白标识符( 1个 )
_
空白标识符有特殊的含义,用来声明-个匿名的变量,该变量在赋值表达式的左端,空白标识符引用通常被用作占位,比如忽略函数多个返回值中的一个和强制编译器做类型检查。
操作符和分隔符
操作符就是语言所使用的符号集合, 包括运算符、显式的分隔符,以及其他语法辅助符号。操作符不但自身是一个 token,具备语法含义,同时其自身也是分隔其他 token 的分隔符。另外还有一类分隔符本身没有什么含义,仅仅起到分隔 token 的功能,这里纯粹的分隔符有 4 个:空格、制表符、回车和换行。
分隔符可以细分如下几类。
- 算术运算符( 5 个 )
算术计算顺序是按照优先级从左到右进行的, 当然也可以使用括号来改变操作数的结合顺序。
+ - * / %
- 位运算符( 6 个 )
& | ^ &^ >> <<
- 赋值和赋值复核运算符 ( 13 个 )
:= = += -= *= /= %= &= |= ^= &^= >>= <<=
- 比较运算符( 6 个 )
> >= < <= == !=
- 括号( 6 个 )
( ) { } [ ]
- 逻辑运算符( 3 个 )
&& || !
- 自增自减操作符( 2 个)
++ --
注意:Go语言里面自增、自减操作符是语句而不是表达式
- 其他运算符( 6 个 )
: , ; . ... <-
字面常量
编程语言源程序中表示固定值的符号叫作字面常量,简称字面量。 一般使用裸字符序列来表示不同类型的值。字面量可以被编程语言编译器直接转换为某个类型的值。 Go 的字面量可以出现在两个地方:一是用于常量和变量的初始化, 二是用在表达式里或作为函数调用实参。变量初始化语句中如果没有显式地指定变量类型 ,则 Go 编译器会结合字面量的值自动进行类型推断。 Go 中的字面量只能表达基本类型的值, Go 不支持用户定义的字面量。
字面量有如下几类:
- 整型字面量
整型字面量使用特定字符序列来表示具体的整型数值,常用于整型变量或常量的初始化。 - 浮点型字面量
浮点型字面量使用特定字符序列来表示一个浮点数值。它支持两种格式 : 一种是标准的数学记录法,比如0.1; 另一种是科学计数法的表示,比如 1E6。 - 复数类型字面量
复数类型字面量使用特定的字符序列来表示复数类型的常量值。 - 字符型字面量
Go 的源码采用的是 UTF心的编码方式, UTF- 8 的宇符占用的字节数可以有 l ~4 个字节,
Rune 宇符常量也有多种表现形式,但是使用“ ’ ’ ”(单引号)将其括住。 - 字符串字面量
字符串字面量的基本表现形式就是使用“ " " ”将字符序列包括在内,双引号里面可以是UTF- 8 的字符字面量 ,也可以是其编码值 。
变量和常量
高级语言通过一个标识符来绑定一块特定的内存,后续对特定的内存的操作都可以使用该标识符来代替。这类绑定某个存储单元的标识符又可以分为两类,一类称之为“变量”,一类称之为“常量”。顾名思义,变量表示指向的内存可以被修改,常量表示指向的内存不能被修改。
变量
变量:使用一个名称来绑定一块内存地址 , 该内存地址中存放的数据类型由定义变量时指定的类型决定,该内存地址里面存放的内容可以改变。
Go 的基本类型变量声明有两种。
1. 显式的完整声明
var varName dataType [ = value]
- 关键字var用于变量声明
- varName是变量名标识符
- dataType是基本类型
- value是变量的初始值,初始值可以是字面量,也可以是其他变量名,还可以是一个表达式;如果不指定初始值,则Go默认将该变量初始化为类型的零值。
- Go的变量声明后就会立即为其分配空间。
var a int = 1
var a int = 2*3
var a int = b
2.短类型声明
varName := value
- := 声明只能出现在函数内(包括在方法内)。
- 此时Go编译器自动进行数据类型推断。
Go支持多个类型变量同时声明并赋值。例如:
a,b := 1,"hello"
变量具有如下几个属性。
- 变量名
Go 中使用自定义标识符来声明一个变量。 - 变量值
变量实际指向的是地址里存放的值,变量的值具体怎么解析是由变量的类型来决定的。在初始化变量值时,我们可以使用字面量,也可以使用其他的变量名。 - 变量存储和生存期
Go 语言提供自动内存管理,通常程序员不需要特别关注变量的生存期和存放位置 。编译器使用栈逃逸技术能够自动为变量分配空间 : 可能在栈上,也可能在堆上 。 - 类型信息
类型决定了该变量存储的值怎么解析,以及支持哪些操作和运算,不同类型的变量支持的操作和运算集是不一样的。 - 可见性和作用域
Go 内部使用统一的命名空间对变量进行管理,每个变量都有一个唯一 的名字,包名是这个名字的前缀。
常量
常量使用一个名称来绑定一块内存地址,该内存地址中存放的数据类型由定义常量时指定的类型决定,而且该内存地址里面存放的内容不可以改变 。 Go 中常量分为布尔型、宇符串型和数值型常量。常量存储在程序的只读段里( .rodata section ) 。
预声明标识符 iota 用在常量声明中,其初始值为 0。一组多个常量同时声明时其值逐行增加, iota 可以看作自增的枚举变量,专 门用来初始化常量。
// 类似枚举的iota
const (
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
// 简写模式
const (
a = iota // a == 0
b // b == 1
c // c == 2
)
// 注意iota 逐行递增
const (
// << 右移多少就是乘多少个2
d = 1 << iota // d == 1 (iota == 0)
e = 1 << iota // e == 2 (iota == 1)
f = 3 // f == 3 (iota == 2, unused)
g = 1 << iota // g == 8 (iota == 3)
)
const (
u = iota * 42 // u == 0 (untyped integer constant)
v float64 = iota * 42 // v == 42.0 (float64 constant)
w = iota * 42 // w == 84 (untyped integer constant)
)
// 分开的const语句,iota计数会被重置为0
const x = iota // x == 0
const y = iota // y == 0
基本数据类型
Go 是一种强类型的静态编译语言,类型是高级语言的基础,有了类型,高级语言才能对不同类型抽象出不同的运算,编程者才能在更高的抽象层次上操纵数据,而不用关注具体存储和运算细节。
- Golang 是强类型语言
- 在赋值过程中, 类型必须保持一致
- 变量必须先定义后使用, 且必须被用到
- Golang 会为每个变量设置默认值
- 变量不能重名
- Golang 会根据值类型做变量类型推断
Go 语言内置七类基本数据类型( 20 个具体子类型)。
布尔类型: bool
整型 : byte int int8 intl6 init32 int64 uint uint8 uintl6 uint32 uint64 uintptr
浮点型 : float32 float64
复数: comlex64 complexl28
字符 : rune
字符串: string
错误类型:error
布尔类型
布尔类型关键字是 bool,布尔类型只有两个值: true 和 fasle,阳e 和 fals巳 是 Go 内置的两
个预声明标识符 。
var ok bool
ok = true
或
ok := false
布尔型数据和整型数据不能进行相互转换。
var a bool
a= 1 //error 1是整型字面量
比较表达式和逻辑表达式的结果都是布尔类型数据 。
var b bool = (x > y) && (x >0)
if 和 for i吾句的条件部分一定是布尔类型的值或表达式 。
if a <= b {
print(b)
else {
print(a)
}
for ; true ; { //等价于 C 语言的 while (1)
}
声明的布尔型变量如不指定初始化值,则默认为 false 。
var b bool // b is fals e
整型
Go 语言内置了12种整数类型,分别是 byte、int 、int8、int16、init32、int64、uint、uint8、uintl6、uint32、uint 64、uintptr。其 中 byte 是 uint8 的别名,不同类型的整型必须进行强制类型转换。
var a int = 1
var b int32 = 2
b = a //error
整型支持算术运算和位操作,算术表达式和位操作表达式的结果还是整型。
var a int = (1+2)*3
var b int = 1000>>2
浮点型
浮点型用于表示包含小数点的数据 , Go 语言内置两种浮点数类型,分别是 float32 和 float64 。浮点数有两个注意事项:
- 浮点数字面量被自动类型推断为 float64 类型。
- 计算机很难进行浮点数的精确表示和存储 , 因此两个浮点数之间不应该使用=或 != 进行比较操作,高精度科学计算应该使用 math 标准库 。
复数类型
Go 语言内置的复数类型有两种,分别是 complex64 和 complex l28,复数在计算机里面使用两个浮点数表示 , 一个表示实部, 一个表示虚部。 complex64 是由两个 float32 构成的, complex l28是 由两个 float64 构成的。复数的字面量表示和数学表示法一样。
var value1 complex64 = 3.2 + 12i
value2 := 3.1 + 6i
//Go有三个内置函数处理复数
//complex、real和imag,分别返回复数的复数、实部和虚部
fmt.Println(complex(3, 2)) // "3+2i"
fmt.Println(real(3 + 2i)) // "3"
fmt.Println(imag(3 + 2i)) // "2"
字符串
Go 语言将字符串作为一种原生的基本数据类型, 字符串的初始化可以使用字符串字面量。
例如 :
var a = "hello,world"
- 字符串是常量,可以通过类似数组 的索引访问其字节单元,但是不能修改某个字节的值。
例如 :
var a = "hello,world "
b := a[0]
a[1] ='a' //error
- 宇符串转换为切片 []byte(s) 要慎用,尤其是当数据量较大时(每转换一次都需复制内容)。
例如:
a := "hello,world!"
b : = []byte(a)
-
字符串尾部不包含 NULL 字符,这一点和 C/C++不一样。
-
字符串类型底层实现是一个二元的数据结构,一个是指针指 向字节数组的起点,另 一个是长度 。
-
基于字符串创建的切片和原字符串指向相同的底层字符数组 , 一样不能修改 , 对字符串的切片操作返回的子串仍然是string,而非 slice。
例如 :
a := "hello,world!"
b := a[0 : 4]
c := a[1 : ]
d := a[ : 4)
- 字符串和切片的转换: 字符串可以转换为字节数组 ,也可以转换为 Unicode 的字数组。
例如 :
a := "hello,世界!"
b := []byte(a)
c := []rune(a)
- 字符串的运算。
例如 :
a := "hello"
b := "world"
// 字符串拼接
c := a + b
println(c)
e := len(a) // 字符串长度
println(e)
d := "hello 世界!"
for i := 0; i < len(d); i++ { // 遍历字节数组
fmt.Println(d[i])
}
for i, ch := range d { // 遍历rune字符数组
fmt.Println(i, ch)
}
rune类型
Go 内置两种字符类型 : 一种是 byte 的字节类类型( byte 是 uint 的别名),另一种是表示Unicode 编码的字符 rune。 rune 在 Go 内部是 int32 类型的别名,占用 4 个字节。 Go 语言默认的字符编码就是 UTF-8 类型的,如果需要特殊的编码转换,则使用 Unicode/UTF-8 标准包。
复合数据类型
复合数据类型就是由其他类型组合而成的类型。 Go 语言基本的复合数据类型有指针、数组、切片、字典( map )、通道、结构和接口,它们的字面量格式如下 :
* pointerType //指针类型使用*后面跟其指向的类型名
[n] elementType //数组类型使用[n]后面跟数组元素类型来表示,n表示该数组的长度
[] elementType //切片类型使用[]后面跟切片元素类型来表示
map [keyType]valueType //map 类型使用 map[键类型]值类型来表示
chan valueType //通道使用 chan 后面跟通道元素类型来表示
struct { //结构类型使用 struct{}将各个结构字段扩起来表示
feildType feildType
feildType feildYype
···
}
interface { //接口类型使用 interface{}将各个方法括起来表示
method1(inputParams)(returnParams)
method1(inputParams)(returnParams)
···
}
指针
Go 语言支持指针,指针的声明类型为*T, Go 同样支持多级指针**T 。通过在变量名前加&来获取变量的地址。指针的特点如下。
- 在赋值语句中,*T 出现在 “=” 左边表示指针声明,*T 出现在 “=” 右边表示取指针指向的值( varName 为变量名)。示例如下 :
var a = 11
p : = &a // *p 和 a 的值都是 11
- 结构体指针访问结构体字段仍然使用“ . ”点操作符, Go 语言没有“ ->”操作符 。 例如 :
type User struct {
name string
age int
}
andes := User{
name : "andes",
age : 18,
}
p := &andes
fmt.Println(p.name) //通过“.”操作符来访问结构体的字段
- Go不支持指针的运算
Go 由于支持垃圾回收,如果支持指针运算,则会给垃圾回收的实现带来很多不便,在 C和 C++里面指针运算很容易出现问题 , 因此 Go 直接在语言层面禁止指针运算。
a := 1234
p := &a
p++ //不允许,报non-numeric type *int 错误
- 函数中允许返回局部变量的地址 。
Go 编译器使用“栈逃逸 ” 机制将这种局部变量的空间分配在堆上。 例如:
func sum(a, b int) *int {
sum := a + b
return &sum //允许,sum 会分配在heap上
}
数组
数组的类型名是[n]elemetType,其中n是数组长度,elementType是数组元素类型。比如一个包含2个int类型元素的数组类型可表示为[2]int。数组一般在创建时通过字面量初始化,单独声明一个数组类型变量而不进行初始化是没有意义的。
数组初始化
var arr [2]int //声明一个有两个整型的数组,但元素默认值都是0,一很少这样使用
array := [...]int{1,2,3} //不指定长度,但是由后面的初始化列表数量来确定其长度
a := [3]int{1:1, 2:3} //指定总长度,并通过索引值进行初始化,没有初始化元素时使用类型默认值
a := [...]int{1:1, 2:3} //不指定总长度,通过索引值进行初始化,数组长度由最后一个索引值确定,没有指定索引的元素被初始化为类型的零值。
数组的特点
- 数组创建完长度就固定了,不可以再追加元素。
- 数组是值类型的,数组赋值或作为函数参数都是值拷贝。
- 数组长度是数组类型的组成部分,[10]int 和 [20]int 表示不同的类型。
- 可以根据数组创建切片。
数组相关操作
- 数组元素访问
a := [...]int{1,2,3}
b := a[0]
for i,v := range a {
}
- 数组长度
a := [...]int{1,2,3}
alengh := len(a)
for i := O; i < alengh ; i++ {
}
切片
Go 语言的数组的定长性和值拷贝限制了其使用场景, Go 提供了另一种数据类型 slice (切片),这是一种变长数组,其数据结构中有指向数组的指针,所以是一种引用类型 。
Go 为切片维护三个元素一一指向底层数组的指针、切片的元素数量和底层数组的容量。
切片的创建
- 由数组创建
创建语法如下:array[b:e],其中,array表示开始索引,可以不指定,默认是0;e表示结束索引,可以不指定,默认是len(array)。array[b:e]表示创建一个包含e-b个元素的切片,第一个元素是array[b],最后一个元素是array[e-1]。例如:
var array = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} //创建有10个int类型的数组
var slice = array[2:5] //创建一个slice,从array[2]开始,到array[5]结束,但不包含array[5]
//slice的长度是3,容量是8
//slice的容量是从它的第一个元素开始,到其底层数组元素末尾的个数
var slice1 = array[2:5:7]
//slice的长度是3,容量是5
var slice2 = array[2:]
//slice的长度是8,容量是8
var slice3 = array[:5]
//slice的长度是5,容量是10
var slice4 = array[:]
//slice的长度是10,容量是10
fmt.Println(slice) //输出[3 4 5]
fmt.Println(slice1) //输出[3 4 5]
fmt.Println(slice2) //输出[3 4 5 6 7 8 9 10]
fmt.Println(slice3) //输出[1 2 3 4 5]
fmt.Println(slice4) //输出[1 2 3 4 5 6 7 8 9 10]
- 通过内置函数make创建切片
注意:由make创建的切片各元素被默认初始化为切片元素类型的零值。例如:
//len = 10, cap = 10
a := make([]int, 10)
//len = 5, cap = 10
b := make([]int, 5, 10)
fmt.Println(a) //输出[0 0 0 0 0 0 0 0 0 0]
fmt.Println(b) //输出[0 0 0 0 0]
// 直接声明切片类型变量是没有意义的
var c []int
fmt.Println(c) //输出[]
切片支持的操作
- 内置函数len()返回切片长度
- 内置函数cap()返回切片底层数组容量
- 内置函数append()对切片追加元素
- 内置函数copy()用于复制一个切片
a := [...]int{0,1,2,3,4,5,6}
b := make([]int,2,3)
c := a[2:5]
fmt.Println(len(b)) //2
fmt.Println(cap(b)) //3
b = append(b, 1)
fmt.Println(b) //[0 0 1]
fmt.Println(len(b)) //3
fmt.Println(cap(b)) //3
b = append(b, c...)
fmt.Println(b) //[0 0 1 2 3 4]
fmt.Println(len(b)) //6
fmt.Println(cap(b)) //6 //容量不够,扩容成原来的2倍,底层的数组也会变成原来的2倍
d := make([]int, 2, 2)
copy(d,c) //copy函数,将c中的元素复制到d中,只会复制长度较小的那个slice的长度
fmt.Println(d) //[2 3]
fmt.Println(len(d)) //2
fmt.Println(cap(d)) //2
字符串和切片的相关转换
str := "hello,世界" //通过字符串字面量初始化一个字符串str
a := []byte(str) //将字符串转换为[]byte类型切片
b := []rune(str) //将字符串转换为[]rune类型切片
map
Go 语言内置的字典类型叫 map。map 的类型格式是: map[K]T ,其中 K 可 以是任意可以进
行比较的类型, T 是值类型。 map 也是一种引用类型 。
map的创建
- 使用字面量创建
map1 := map[string]int{"a":1, "b":2, "c":3}
fmt.Println(map1["a"]) //输出1
fmt.Println(map1["b"]) //输出2
- 使用内置的make函数创建
make(map[K]T) //map 的容量使用默认位
make(map[K]T,len) //map 的容量使用给定的 len 值
map2 := make(map[int]string)
map2[1] = "a"
map2[2] = "b"
map2[3] = "c"
fmt.Println(map2[1]) //a
fmt.Println(map2[2]) //b
map支持的操作
- map 的单个键值访问格式为 mapName[key], 更新某个 key 的值时 mapName[key]放到等号左边,访 问某个 key 的值时 mapName[key]放在等号的右边。
- 可以使用 range 遍历一个 map 类型变量,但是不保证每次选代元素的顺序。
- 删除 map 中的某个键值 , 使用如下语法: delete(mapName,key) 。 delete 是内置函数,用来删除 map 中的某个键值对 。
- 可以使用内置的 len()函数返回 map 中的键值对数量。
mp := make(map[int]string)
mp[1] = "tom"
mp[1] = "pony"
mp[2] = "jaky"
mp[3] = "andes"
delete (mp , 3)
fmt.Println(mp[1])
fmt.Println(len(mp)) //len 函数返回 map 中的键值对的数量
for k, v := range mp { //range 支持遍历 mp ,但不保证每次遍历次序是一样的
fmt.Println("key=", k, "value=", v)
}
- Go 内置的 map 不是并发安全的,并发安全的 map 可以使用标准包 sync中的 map 。
- 不要直接修改 map value 内某个元素的值,如果想修改 map 的某个键值,则必须整体赋
值。例如 :type User struct{ Name string age int } ma := make(map[int]User) andes := User{"andes", 18} ma[1] = andes // ma[1].age = 20 //error 不能通过map引用直接修改结构体的值 andes.age = 20 fmt.Println(ma[1].age) //18 ma[1] = andes //必须整体替换value fmt.Println(ma[1].age) //20
struct
Go 中的 struct 类型和 C 类似,由多个不同类型元素组合而成。这里面有两层含义:第一 ,struct 结构中的类型可以是任意类型:第二,struct 的存储空间是连续的,其字段按照声明时的顺序存放(注意字段之间有对齐要求)。
struct 有两种形式:一种是 struct 类型字面量,另一种是使用 type 声明的自定义 struct 类型。
struct类型字面量
声明格式如下:
struct {
FeildName FeildType
FeildName FeildType
FeildName FeildType
}
自定义struct类型
声明格式如下:
type TypeName struct {
FeildName FeildType
FeildName FeildType
FeildName FeildType
}
实际使用 struct 字面量的场景不多,更多的时候是通过 type 自定义一个新的类型来实现的 。Type 是自定义类型的关键字,不但支持 struct 类型的创建,还支持任意其他子定义类型的创建。
struct类型变量的初始化
type Person struct {
Name string
Age int
}
type Student struct {
*Person
Number int
}
// 按照类型声明顺序,逐个赋值
// 不推荐这种初始化方式,一旦struct的定义发生变化,这里的初始化也要跟着变化
a := Person{"andes", 18}
// 推荐使用Feild:value的方式初始化,这样即使struct定义发生变化,初始化的顺序也不会变化
b := Person{
Name: "andes",
Age: 18,
}
s := Student {
Person: p,
Number: 001,
}
其他复合类型
接口、通道之后介绍
控制结构
程序执行从本质上来说就是两种模式:顺序和跳转。
- 顺序就是按照程序指令在存储器上的存放顺序逐条执行。
- 跳转就是遇到跳转指令就跳转到某处继续线性执行。
Go 是一门高级语言,其源程序虽然经过了高度的抽象并封装了很多语法糖,但还是跳不出这个模式(这里暂时不考虑 goroutine 引入并发后的执行视图变化) 。
顺序在 Go 里面体现在从 main 函数开始逐条向下执行,就像我们的程序源代码顺序一样 ;跳转在 Go 里面体现为多个语法糖,包括 goto 语句和函数调用 、 分支( if、 switch 、 select ) 、循环( for )等。跳转分为两种: 一种是无条件跳转,比如函数调用和 goto 语句;一种是有条件的跳转,比如分支和循环 。
顺序语句很简单,就是我们天然写程序的从前往后的顺序。
Go 的源代码的顺序并不一定是编译后最终可执行程序的指令顺序,这里面涉及语言的运行时和包的加载过程 。 上面论述的主要目的是使读者从宏观上整体理解程序的执行过程,建立一个从源代码到执行体的大体映射概念,这个概念不那么精准,但对我们理解源程序到目标程序的构建非常有帮助。
if 语句
特点
- if后面的条件判断子句不需要小括号括起来。
- { 必须放在行尾,和 if 或 if else 放在一行。
- if 后面可以带一个简单的初始化语句,并以分号分割,该简单语句声明的变量的作用域是整个 if 语句块,包括后面的 else if 和 else 分支。
- Go语言没有条件运算符(三目运算),这也符合Go的设计哲学,只提供一种方法做事情。
- if 分支语句遇到return 后直接返回,遇到 break 则跳过 break 下方的 if 语句块。
a := 1
if a > 0 {
println("a是正数")
} else if a < 0 {
println("a是负数")
} else {
println("a是0")
}
//在初始化语句中声明变量
if b := 1; b > 0 {
println("b是正数")
} else {
println("b是非正数")
}
- 尽量减少条件语句的复杂度,如果条件语句太多、太复杂,则建议放到函数里面封装起来。
- 尽量减少 if 语句的嵌套层次,通过重构让代码变得扁平,便于阅读。
switch 语句
switch 语句会根据传入的参数检测井执行符合条件的分支。
switch 的语法特点如下:
- switch 和 if 语句一样, switch 后面可以带一个可选的简单的初始化语句。
- switch 后面的表达式也是可选的,如果没有表达式, 则 case 子句是一个布尔表达式 ,而不是一个值,此时就相当于多重 if else 语句。
- switch 条件表达式的值不像 C 语言那样必须限制为整数, 可以是任意支持相等比较运算的类型变量。
- 通过 fallthough 语句来强制执行下一个 case子句(不再判断下一个 case 子句的条件是否满足)。
- switch 支持 default 语句, 当所有的 case 分支都不符合时, 执行 default 语句, 并且 default语句可以放到任意位置,并不影响 switch 的判断逻辑。
- switch 和 .(type) 结合可以进行类型的查询。
switch i := "y"; i { //switch后面可以带上一个初始化语句,初始化语句和if一样,作用域只在switch内部
case "y", "Y": //多个条件用逗号分隔
println("yes")
fallthrough //fallthrough语句会强制执行后面的case代码
case "n", "N":
println("no")
}
//输出yes no
score := 85
grade := ""
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
case score >= 70:
grade = "C"
case score >= 60:
grade = "D"
default:
grade = "F"
}
fmt.Println("grade:", grade) //grade: B
for 语句
Go 语言仅支持一种循环语句, 即 for 语句,同样遵循 Go 的设计哲学,只提供一种方法做事情,把事情做好。
- 类似 C 里面的 for 循环语句
for init; condition; post {}
- 类似 C 里面的 while 循环语句
for condition {}
- 类似 C 里面的 while(1) 死循环语句
for {}
for 还有一种用法,是对数组、切片、宇符串、 map 和通道的访问,语法格式如下:
// for循环访问map
for key, value := range map[string]int{"one": 1, "two": 2, "three": 3} {
println(key, value)
}
// for循环访问array
for i, v := range [3]int{1, 2, 3} {
println(i, v)
}
// for循环访问slice
for i, v := range []int{1, 2, 3, 4} {
println(i, v)
}
// for循环访问字符串
for i, c := range "go" {
println(i, c)
}
// for循环访问channel
channel := make(chan int)
for value := range channel {
println(value)
}
标签和跳转
标签
Go 语言使用标签 ( Lable )来标识一个语句的位置,用于 goto 、 break 、 continue 语句的跳转。标签的语法是:
Lable: Statement
标签的具体作用和使用见下面的 goto 、 break 、 continue 。
goto
goto 语句用于函数的内部的跳转,需要配合标签一起使用, 具体的格式如下:
goto Lable
goto Lable 的语义是跳转到标签名后的语句处执行, goto 语句有以下几个特点:
- goto 语句只能在函数内跳转。
- goto 语句不能跳过内部变量声明语句,这些变量在 goto 语句的标签语句处又是可见的。
例如:
goto L //BAD,跳过v := 3 这条语句是不允许的
v := 3
L:
- goto 语句只能跳到同级作用域或者上层作用域内,不能跳到内部作用域内。
例如:
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1: //Unused label 'L1'
f()
n--
}
break
break 用于函数内跳出 for、switch、select 语句的执行,有两种使用格式:
- 单独使用,用于跳出 break 当前所在的 for、switch、select 语句的执行。
- 和标签一起使用,用于跳出标签所标识的 for、switch、select 语句的执行,可用于跳出多重循环,但标签和 break 必须在同一个函数内。
例如:
L1:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i == 3 {
fmt.Println("i:", i, "j:", j)
break L1 //跳出到L1标签
}
if j == 5 {
break //跳出离它最近的for循环,j == 5之后的不再打印
fmt.Println("i:", i, "j:", j)
}
fmt.Println("i:", i, "j:", j)
}
}
continue
continue 用于跳出 for 循环的本次选代,跳到 for 循环的下一次选代的 post 语句处执行,也有两种使用格式 :
- 单独使用,用于跳出 continue 当前所在的 for 循环的本次迭代。
- 和标签一起使用,用于跳出标签所标识的 for 语句的本次选代,但标签和 continue 必须在同一个函数内。
例如:
L2:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i == 2 {
continue L2 //跳出到L1标签所在的for循环i++处执行
fmt.Println("i:", i, "j:", j)
}
if j == 5 {
continue //跳出离它最近的for循环,j == 5 之后的会接着打印
fmt.Println("i:", i, "j:", j)
}
fmt.Println("i:", i, "j:", j)
}
}
return 和函数调用
return语句也能引发控制流程的跳转,用于函数和方法的退出。函数和方法的调用也能引发程序控制流的跳转。
总结
通过本篇博客的介绍,我们了解了GO语言的基础语法特性,包括简洁的变量声明与赋值、灵活的控制结构。GO语言的设计目标是简洁高效,其语法特性使得代码编写和阅读更加舒适,也有助于提高代码的可维护性。让我们一起享受GO语言编程的乐趣吧!