3.1 保留字
Go语言仅25个保留关键字,关键字不能用作常量、变量、函数名、以及结构体等标识符
3.2 运算符
除位移运算符外,操作数据类型必须相同。如果其中一个是无显示类型声明的常量,那么它会自动转型
package main
import "fmt"
func main() {
const v = 20
var a byte = 10
d := a + v
fmt.Printf("%T , %v\n", d, d)
const c float32 = 1.2
e := v + c
fmt.Printf("%T , %v\n", e, e)
}
结果:
uint8 , 30
float32 , 21.2
位移右操作符必须是无符号整数,或可以转换的无显示类型常量
package main
func main() {
b := 23
x := 1 << b //invalid operation: 1 << b (shift count type int, must be unsigned integer)
println(x)
}
自增
自增、自减不再是运算符。只能作为独立语句,不能用于表达式
package main
func main() {
a := 1
//++a //syntax error: unexpected ++, expecting }
//if (a++) >1{ //syntax error: unexpected ++, expecting )
//}
p := &a
*p++
println(a) //2
}
指针
不能将内存地址与指针混为一谈
内存地址是内存中每个字节单元的唯一编号,而指针会分配内存空间,相当于一个专门用来保存地址的整型变量
- 取地址运算符“&”用于获取对象地址
- 指针运算符“*”用于间接引用目标对象
- 二级指针**T,如包含包名则写成*package.T
并非所有对象都能进行取地址操作,但变量总是能正确返回(addressable)。指针运算符为左值时,我们可更新目标对象状态,而为右值时则是为了获取目标状态
package main
func main() {
x := 10
var p *int = &x
*p += 20
println(x,&x,p,*p)
}
result:
30 0xc00002df80 0xc00002df80 30
package main
func main() {
var m = make(map[string]int)
m["a"] = 1
println(&m["a"]) //cannot take the address of m["a"]
}
指针类型支持相等运算符,但不能做加减法运算和类型转换。如果两个指针指向同一地址,或都为nil,那么他们相等。
package main
func main() {
x := 10
p := &x
//p ++ // invalid operation: p++ (non-numeric type *int)
//var p2 *int = p + 1 // invalid operation: p + 1 (mismatched types *int and int)
p2 := &x
println(p == p2) //true
}
可通过unsafe.Pointer 将指针转换为uintptr后进行加减法运算,但可能会造成非法访问。
Pointer 类似C语言中的void*万能指针,可用来转换指针类型。它能安全持有对象或对象成员,但uintptr不行,后者仅是一种特殊整型,并不能引用目标对象,无法阻止垃圾回收器回收对象内存。
指针没有专门指向成员的“->”运算符,统一使用“.”选择表达式
package main
func main() {
x := struct{
a int
}{}
x.a = 100
p := &x
p.a += 100
println(p.a) //200
}
零长度对象的地址是否相等和具体的实现版本有关,不过,肯定不等于nil
package main
func main() {
var a, b struct{}
println(&a, &b) //0xc00002df88 0xc00002df88
println(&a == &b, &a == nil) //false false
}
即便长度是0,可该对象依然是“合法的存在”,拥有合法内存地址,这与nil语义完全不同。
3.3 初始化
对复合类型(数组、切片、字典、结构体)变量初始化时,有一些语法限制。
- 初始化表达式必须包含类型标签。
- 左花括号必须在类型尾部,不能另起一行
- 多个成员初始值以逗号分开
- 允许多行,但每行须以逗号或右花括号结束
package main
import "fmt"
func main() {
type data struct {
x int
y string
}
var a data = data{1, "s"}
b := data{
1,
"asdf",
}
c := []int{
1,
2,
3,
}
d := []int{1, 2, 3,
4,
5,
}
fmt.Println(a, b, c, d)
}
3.4 流程控制
GO 精简了流控制语句,虽然某些时候不够便捷,但够用
package main
func main() {
x := 3
if x > 5 {
println("a")
} else if x <= 5 && x > 0 {
println("b")
} else {
println("c")
}
}
比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。
package main
func xint() {
println("a")
}
func main() {
x := 10
if xint(); x == 0 {
println("b")
}
if a, b := x+1, x+10; a < b {
println(a)
} else {
println(b)
}
}
结果
a
11
局部变量的有效范围包括整个if/else块
死代码是永远不会被执行的代码,可使用专门的工具或用代码覆盖率测试
尽量减少代码块嵌套,让逻辑出于正常水平
package main
import (
"errors"
"log"
)
func check(x int) error {
if x <= 0 {
return errors.New("x < 0")
}
return nil
}
func main() {
x := 10
if err := check(x); err == nil {
x++
println(x)
} else {
log.Fatalln(err)
}
}
该例中,if块显然承担了两种逻辑:错误处理和后续正常操作。基于重构原则,我们应保持代码功能的单一性。
func main() {
x := 10
if err := check(x); err != nil {
log.Fatalln(err)
}
x++
println(x)
}
如此,if块仅完成条件检查和错误处理,相关正常逻辑保持在同一层次。当有人试图通过这段代码来获知逻辑流程时,完全可以忽略if块细节。同时,单一功能可提升代码可维护性,更利于拆分重构。
当然,如须在多个条件块中使用局部变量,那么只能保留原层次,或直接使用外部变量
package main
import (
"log"
"strconv"
)
func main() {
s := "g"
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
log.Fatalln(err)
} else if n < 0 || n > 10 {
log.Fatalln("invalid number")
}
println(n)
}
对于某些过于复杂的组合条件,建议将其重构为函数
package main
import (
"log"
"strconv"
)
func main() {
s := "g"
if n, err := strconv.ParseInt(s, 10, 64); err != nil || n < 0 || n > 10 || n%2 != 0 {
log.Fatalln("invalid number")
}
println("ok")
}
package main
import (
"errors"
"log"
"strconv"
)
func check(s string) error {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || n < 0 || n > 10 || n%2 != 0 {
return errors.New("invalid num")
}
return nil
}
func main() {
s := "g"
if err := check(s); err != nil {
log.Fatalln(err)
}
println("ok")
}
将流程和局部细节分离是很常见的做法,不同的变化因素被分隔在各自独立单元内,可避免修改时造成关联错误,减少“肥胖症”的函数数量。当然,代码单元测试也是主要原因之一。另一方面,该示例中的函数check仅被if模块调用,也可以将其作为局部函数,以避免扩大作用域,只是对测试的友好度会差一些
switch
与if语句类似,switch语句也用于选择执行,但具体使用的场景会有所不同
package main
func main() {
a, b, c, x := 1, 2, 3, 2
switch x {
case a, b:
println("a | b")
case c:
println("c")
case 4:
println("d")
default:
println("z")
}
}
条件表达式支持非常量值,这要比C更加灵活。相比if表达式,switch值列表要更加直观
编译器对if、switch生成的机器指令可能完全相同,所谓谁性能更好须看具体情况,不能作为主观判断条件。
switch同样支持初始化语句,按从上到下,从左到右顺序匹配case执行,只有全部匹配失败时才会执行default块
package main
func main() {
switch x := 5; x {
default: // 不会先执行default
x += 100
println(x)
case 5:
x += 50
println(x)
}
}
结果:55
考虑到default与else类似,建议将其放在switch末尾
相邻的空case不构成多条件匹配
package main
func main() {
switch x := 10; x {
case 10: // 单条件,内容为空。隐式:“case 10: break”
case 20:
println(20)
}
}
不能出现重复的case值:
package main
func main() {
switch x := 10; x {
case 10:
println(10)
case 20, 10: //duplicate case 10 in switc
println(20)
}
}
无需显示执行break语句,case执行完毕后制动中断。如需贯通后续case(源码顺序),
须执行fallthrough,但不再匹配后续条件表达式
package main
func main() {
switch x := 5; x {
default:
println(x)
case 5:
x += 10
println(x) //15
fallthrough // 继续执行下一case,但不再匹配表达式
case 6:
x += 20
println(x) //35
//fallthrough //cannot fallthrough final case in switch
}
}
注意,fallthrough 必须放在case块结尾,可使用break语句阻止
某些时候,switch语句还被用于替换if语句。被省略的switch条件表达式默认值为true,
继而与case比较表达式结果匹配
package main
func main() {
switch x := 5; {
case x > 5:
println("a")
case x > 0 && x < 5:
println("b")
default:
println("c")
}
}
for
package main
func main() {
for i:=0 ; i< 10 ; i++{
}
i:=1
for i<10 { // 类似 “while x <10 {}”
i++
}
for {
break
}
}
初始化语句仅被执行一次。表达式中如果有函数调用,须确认是否会重复执行。可能会被编译器优化掉,也可能是动态结果须每次确认
package main
func count() int {
println("count,")
return 3
}
func main() {
for i, c := 0, count(); i < c; i++ {
println("a", i)
}
c := 0
for c < count() { // 这里的函数会重复执行
println("b", c)
c++
}
}
规避方式就是在初始化表达式中定义局部变量保存count结果
可用for…range完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据
package main
import (
"fmt"
)
func main() {
s := "asdfsdf"
for i, j := range s {
fmt.Println(i, j)
}
a := [3]string{"a", "b", "c"}
for i, j := range a {
fmt.Println(i, j)
}
b := []string{"x", "x", "x", "y"}
for i, j := range b {
fmt.Println(i, j)
}
e, f, g := 1, 2, 3
d := [3]*int{&e, &f, &g}
for i, j := range d {
fmt.Println(i, j)
}
var h = make(map[string]int)
h["a"] = 1
h["b"] = 2
h["c"] = 3
h["d"] = 4
for i, j := range h {
fmt.Println(i, j)
}
}
允许返回单值,或用“_”忽略。
package main
import (
"fmt"
)
func main() {
data := [3]string{"a", "b", "c"}
for i := range data { // 忽略值
fmt.Println(i)
}
for _, j := range data { // 忽略索引
fmt.Println(j)
}
for range data { //仅迭代,不返回。可用来执行清空channel等操作
}
}
无论是普通for循环,还是range迭代,其定义的局部变量都会重复使用
package main
import "fmt"
func main() {
var data = [3]string{"a", "b", "c"}
for i, s := range data {
fmt.Println(&i, &s)
}
}
0xc00004c058 0xc0000401c0
0xc00004c058 0xc0000401c0
0xc00004c058 0xc0000401c0
如果range目标表达式是函数调用,也仅被执行一次。
package main
func data() []int {
return []int{1, 2, 3, 4, 5}
}
func main() {
for i, x := range data() {
println(i, x)
}
}
goto,continue,break
使用goto前,须先定义标签。标签区分大小写,且未使用的标签会引发编译错误。
func main() {
for i := 0; i < 3; i++ {
println(i)
if i >= 1 {
goto exit
}
}
exit:
println("exit.")
}
0
1
exit.
不能跳转到其他函数,或内层代码块内
- break: 用于switch、for、select语句,终止,整个语句块执行。
- continue:仅用于for循环,终止后续逻辑,立即进入下一轮循环
func main() {
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
if i > 5 {
break
}
println(i)
}
}
配合标签,break,continue可在多层嵌套中指定目标层级
func main() {
outer:
for x := 0; x < 5; x++ {
for y := 0; y < 10; y++ {
if y > 2 {
println()
continue outer
}
if x > 2 {
break outer
}
print(x, ":", y, " ")
}
}
}
结果
0:0 0:1 0:2
1:0 1:1 1:2
2:0 2:1 2:2