Skip to content

基本语法

变量

定义

关键字 var 用于定义变量,和 C 不同,类型被放在变量名后。
另外,运行时内存分配操作会确保变量自动初始化为二进制零值(zero value),避免出现不可预测行为。
如显式提供初始化值,可省略变量类型,由编译器推断

1
2
var x int // 自动初始化为 0
var y = false // 自动推断为 bool 类型

可一次定义多个变量,包括用不同初始值定义不同各类型

1
2
var x, y int  // 相同类型的多个变量
var a, s = 100, "abc"  // 不同类型初始化值

依照惯例,建议以组方式整理多行变量定义

1
2
3
4
var (
    x, y int
    a, s = 100, "abc"
)

简短模式

除 var 关键字外,还可使用更加简短的变量定义和初始化语法

1
2
3
4
func main() {
    x := 100
    a, s := 1, "abc"
}

只是要注意,简短模式(short variable declaration)有些限制:

  • 定义变量,同时显式初始化
  • 不能提供数据类型
  • 只能用在函数内部。

对于粗心的新手,这可能会造成意外错误。
比如原来打算修改全部变量,结果变成重新定义同名局部变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

var x = 100

func main() {
    println(&x, x) // 全局变量

    x := "abc" // 重新定义和初始化同名局部变量
    println(&x, x)
}

输出结果:

1
2
3
// 对比内存地址,可以看出是两个不同的变量
0x10c8190 100  
0xc000038768 abc

简短定义在函数多返回值,以及if/for/switch等语句中定义局部变量非常方便

简短模式并不是重新定义变量,有可能是部分退化的赋值操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func main() {
    x := 100
    println(&x)

    x, y := 200, "abc" // 注意: x 退化为赋值操作,仅有 y 是变量定义

    println(&x, x)
    println(y)
}

输出:

1
2
3
0xc000038770
0xc000038770 200
abc

退化赋值的前提条件是: 最少有一个新变量被定义,且必须是同一作用域

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

func main() {
    x := 100
    println(&x)

    x := 200 // 错误:no new variables on left side of :=

    println(&x, x)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func main() {
    x := 100
    println(&x)

    {
        x, y := 200, 300 // 不同作用域,全部是新变量定义
        println(&x, x, y)
    }
}

输出:

1
2
0xc000038770
0xc000038768 200 300

在处理函数错误返回值时,退化赋值允许我们重复使用 err 变量,这是相当有益的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "log"
    "os"
)

func main() {
    f, err := os.Open("dev/random")
    ...

    buf := make([]byte, 1024)
    n, err := f.Read(buf) // err 退化赋值,n 新定义
    ...
}

多变量赋值

在进行多变量赋值操作时,首先计算出所有右值,然后再依次完成赋值操作

1
2
3
4
5
6
7
8
9
package main

func main() {
    x, y := 1, 2
    x, y = y+3, x+2 // 先计算出右值 y + 3、x + 2,然后再对 x、y 变量赋值

    println(x, y)
}
// 输出结果: 5 3

编译器将未使用局部变量当作错误。不要觉得麻烦,这有助于培养良好的编码习惯

1
2
3
4
5
6
7
package main

var x int // 全局变量没问题

func main() {
    y := 10
}
1
./test.go:6:2: y declared but not used

Go 语言的每一个变量都拥有自己的类型,必须经过声明才能开始用

1
2
3
4
5
6
7
var a int
var b string
var c []float32
var d func() bool
var e struct {
    x int
}

上面代码的共性是,以 var 关键字开头,要声明的变量名放在中间,而将其类型放在后面

批量定义变量的方法

1
2
3
4
5
6
7
8
9
var (
    a int 
    b string
    c []float32
    d func() bool
    e struct {
        x int
    }
)

初始化变量

Go 语言在声明变量时,自动对变量对应的内存区域进行初始化操作。

每个变量会初始化其类型的默认值

  • 整形和浮点型变量的默认值为 0
  • 字符串变量的默认值为空字符串
  • 切片、函数、指针变量的默认为 nil

当然,依然可以在变量声明时赋予变量一个初始值

回顾:
在 C 语言中,变量在声明时,并不会对变量对应的内存区域进行清理操作。
此时,变量值可能是完全不可预期的结果。
开发者需要习惯在使用 C 语言进行声明时要初始化操作,稍有不慎,就会造成不可预知的后果

在网络上只有程序员才能看懂的“烫烫烫”和“屯屯屯”的梗,就来源于 C/C++ 中变量默认不初始化
微软的 VC 编译器会将未初始化的栈空间以 16 进制的 0xCC 填充,而未初始化的堆空间使用 0xCD 填充,而 0xCCCC 和 0xCDCD 在中文的 GB2312 编码中刚好对应 “烫” 和“屯”字
因此,如果一个字符串没有结束符 "\0",直接输出的内存数据转换为字符串刚好对应“烫烫烫”和“屯屯屯”

标准格式

1
var 变量名 类型 = 表达式

例如:游戏中,玩家的血量初始值为 100

1
var hp int = 100

编译器推导类型的格式

在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型

1
var hp = 100

等号右边的部分在编译原理里被称作“右值”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

var aa = 3
var kk = "kkk"

// bb := true 这种全局写法是不行的, := 只能在函数内使用
var bb = true

var (
    x = 3
    y = "kkk"
    z = true
)

func variableZeroValue() {
    var a int
    var s string
    fmt.Printf("%d %q\n", a, s)
}

func varibaleInitialValue() {
    var a, b int = 3, 4
    var s string = "abc"
    fmt.Println(a, b, s)
}

func variableTypeDecuction() {
    var a, b, c, s = 3, 4, true, "def"
    fmt.Println(a, b, c, s)
}

func variableShorter() {
    a, b, c, s := 3, 4, true, "def"
    b = 5
    fmt.Println(a, b, c, s)
}

func main() {
    fmt.Println("Hello World")
    variableZeroValue()
    varibaleInitialValue()
    variableTypeDecuction()
    variableShorter()
    fmt.Println(x, y, z)
}
// Hello World
// 0 ""
// 3 4 abc
// 3 4 true def
// 3 5 true def
// 3 kkk true

命名

对变量、常量、函数、自定义类型进行命名,通常优先选用有实际含义,易于阅读和理解的字母或单词组合

命名建议

  • 以字母或下划线开始,由多个字母、数字和下划线组合而成
  • 区分大小写
  • 使用驼峰(camel case)拼写格式
  • 局部变量优先使用短名
  • 不要使用保留关键字
  • 不建议使用与预定义常量、类型、内置函数相同的名字
  • 专用名词通常会全部大写

尽管 Go 支持用汉字等 Unicode 字符命名,但从编程习惯上来说,这并不是好选择
符号名字首字母大小写决定了其作用域。
首字母大写的为导出成员,可被包外引用,而小写则仅能在包内使用

名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;
你会经常看到i之类的短名字,而不是冗长的 theLoopIndex 命名。
通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

在习惯上,Go语言程序员推荐使用驼峰式命名,当名字有几个单词组成的时优先使用大小写分隔,而不是优先用下划线分隔。
因此,在标准库有 QuoteRuneToASCIIparseRequestLine 这样的函数命名,但是一般不会用 quote_rune_to_ASCIIparse_request_line 这样的命名。
而像 ASCIIHTML 这样的缩略词则避免使用大小写混合的写法,它们可能被称为 htmlEscapeHTMLEscapeescapeHTML,但不会是 escapeHtml

空标识符

和 Python 类似,Go 也有个名为_的特殊成员。
通常作为忽略占位符使用,可作表达式左值,无法读取内容。

1
2
3
4
5
6
import "strconv"

func main() {
    x, _ := strconv.Atoi("12")  // 忽略 Atoi 的 err 返回值
    println(x)
}

空标识符可用来临时规避编译器未使用变量和导入包的错误检查。
但请注意,它是预置成员,不能重新定义。

常量与枚举

常量

常量表示运行时恒定不可改变的值,通常是一些字面量。
使用常量就可用一个易于阅读理解的标识符号来代替“魔法数字”,也使得在调整常量值时,无须修改所有引用代码。

常量值必须是编译器可确定的字符、字符串、数字或布尔值。
可指定常量类型,或由编译器通过初始化值推断,不支持 C/C++ 数字类型后缀

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。

一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。
例如,常量比变量更适合用于表达像 π 之类的数学常数,因为它们的值不会发生变化:

1
const pi = 3.14159 // approximately; math.Pi is a better approximation

和变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量:

1
2
3
4
const (
    e  = 2.71828182845904523536028747135266249775724709369995957496696763
    pi = 3.14159265358979323846264338327950288419716939937510582097494459
)

所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。
当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:lencaprealimagcomplexunsafe.Sizeof

因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:

1
2
3
4
5
6
7
const IPv4Len = 4

// parseIPv4 parses an IPv4 address (d.d.d.d).
func parseIPv4(s string) IP {
    var p [IPv4Len]byte
    // ...
}

一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。
在下面的代码中,time.Duration是一个命名类型,底层类型是int64,time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型,可以通过%T参数打印类型信息:

1
2
3
4
5
const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay)     // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout)     // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:

1
2
3
4
5
6
7
8
const (
    a = 1
    b
    c = 2
    d
)

fmt.Println(a, b, c, d) // "1 1 2 2"

如果只是简单地复制右边的常量表达式,其实并没有太实用的价值。但是它可以带来其它的特性,那就是iota常量生成器语法。

1
2
3
4
5
6
7
8
const x, y int = 123, 0x22
const s = "hello, world!"
const c = '我'  // rune(unicode code point)

const (
    i, f = 1, 0.123 // int, float64(默认)
    b = false
)

可在函数代码块中定义常量,不曾使用的常量不会引发编译错误.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

func main() {
    const x = 123
    println(123)

    const y = 1.23 // 未使用,不会引发编译错误
    {
        const x = "abc" // 在不同作用域定义同名常量
        println(x)
    }
}

如果显式指定类型,必须确保常量左右值类型一致,需要时可做显式转换。
右值不能超出常量类型取值范围,否则会引发溢出错误

1
2
3
4
5
const (
    x, y int  = 99, -999
    b    byte = byte(x) // x 被指定为 int 类型,须显式转换为 byte 类型
)
n = uint8(y) // 错误: constant -99 overflows uint8

常量值也可以是某些编译器能计算出结果的表达式,如 unsafe、Sizeof、len、cap 等

1
2
3
4
5
6
import "unsafe"

const (
    ptrSize = unsafe.Sizeof(uintptr(0))
    strSize = len("hello,world!")
)

在常量组中如不指定类型和初始化值,则与上一行非空常量右值(表达式文本)相同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func main() {
    const (
        x uint16 = 120
        y        // 与上一行 x 类型、右值相同
        s = "abc"
        z // 与 s 类型、右值相同
    )
    fmt.Printf("%T, %v\n", y, y) // 输出类型和值
    fmt.Printf("%T, %v\n", z, z)
}

输出:

1
2
uint16, 120
string, abc
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
    "fmt"
    "math"
)

func consts() {
    // const filename = "abc.txt"
    // const a, b = 3, 4
    const (
        filename = "abc.txt"
        a, b     = 3, 4
    )
    var c = int(math.Sqrt(a*a + b*b))
    fmt.Println(filename, c)
}

func main() {
    consts()
}
// abc.txt 5 

枚举

Go 并没有明确意义上的 enum 定义,不过可借助 iota 标识符实现一组自增常量值来实现枚举类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const (
    x = iota // 0
    y // 1
    z // 2
)

const (
    _ = iota //0
    KB = 1 << (10 * iota) // 1 << (10 * 1)
    MB  // 1 << (10 * 2)
    GB  // 1 << (10 * 3)
)

自增作用范围为常量组。可在多常量自定义中使用多个 iota,它们各自单独计数,只需确保组中每行常量的列数量相同即可

1
2
3
4
5
const (
    _, _ = iota, iota * 10 // 0, 0 * 10
    a, b // 1, 1 * 10
    c, d // 2, 2 * 10
)

如中断 iota 自增,则必须显式恢复。且后续自增值按行序递增

1
2
3
4
5
6
7
8
const (
    a = iota // 0
    b // 1
    c = 100 // 100
    d // 100(与上一行常量右值表达式相同)
    e = iota // 4(恢复 iota 自增,计数包括 c、d)
    f // 5
)

自增默认数据类型为 int,可显式指定类型

1
2
3
4
5
const (
    a = iota // int
    b float32 = iota // float32
    c = iota // int(如不显式指定 iota,则与 b 数据类型相同)
)

在实际编码中,建议用自定义类型实现用途明确的枚举类型。
但这并不能将取值范围限定在预定义的枚举值内

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type color byte // 自定义类型

const (
    black color = iota  // 指定常量类型
    red
    blue
)

func test(c color) {
    println(c)
}

func main() {
    test(red)
    test(100) // 100 并未超出 color/byte 类型取值范围

    x := 2
    test(x) // 错误: cannot use x(type int) as type color in argument to test
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
    "fmt"
)

func enums() {
    // const (
    //  cpp    = 0
    //  java   = 1
    //  python = 2
    //  golang = 3
    // )
    const (
        cpp = iota
        java
        python
        golang
        javascript
    )
    const (
        b = 1 << (10 * iota)
        kb
        mb
        gb
        tb
        pb
    )
    fmt.Println(cpp, java, python, golang)
    fmt.Println(b, kb, mb, gb, tb, pb)
}

func main() {
    enums()
}
// 0 1 2 3
// 1 1024 1048576 1073741824 1099511627776 1125899906842624

不同于变量在运行期分配存储内存(非优化状态),常量通常会被编译器在预处理阶段直接展开,作为指令数据使用

枚举类型判断是否相等:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

type color byte // 自定义类型

const (
    black color = iota // 指定常量类型
    red
    blue
)

func test(c color) {
    println(c)
    println(c == blue)
    println(c == red)
}

func main() {
    test(red)
}

输出结果:

1
2
3
1
false
true

iota

我们知道 iota 常用于 const 表达式中,其值是从 0 开始的,const 声明块中每增加一行,iota 值自增 1

使用 iota 可以简化常量定义,但其规则必须要牢牢掌握,否则在阅读源码时可能会造成误解或障碍

(1) 下面每个常量的值是多少?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Priority int
const (
    LOG_EMERG Priority = iota
    LOG_ALERT
    LOG_CRIT
    LOG_ERR
    LOG_WARNING
    LOG_NOTICE
    LOG_INFO
    LOG_DEBUG
)

解答:
以上常量声明来源于 Go 标准库 syslog,每个常量代表一个日志级别,常量类型为 Priority,实际为 int 类型

iota 在常量声明语句汇总的初始值为 0,即 LOG_EMERG 的值为 0,下面每个常量递增 1

(2) 下面每个常量的值是多少?

1
2
3
4
5
6
7
const (
    mutexLocked = 1 << iota  // mutex is locked
    mutexWoken
    mutexStarving
    mutexWaiterShift = iota
    starvationThresholdNs = 1e6
)

解答:
以上常量声明来源于 Go 标准库 sync,每个常量均代表互斥锁的特定状态

  • mutexLocked 的值为 1
  • mutexWoken 的值为 2
  • mutexStarving 的值为 4
  • mutexWaiterShift 的值为 3
  • srarvationThresholdNs 的值为 1000000

(3) 下面每个常量的值是多少?

1
2
3
4
5
6
const (
    bit0, mask0 = 1 << iota, 1 << iota - 1
    bit1, mask1
    _, _
    bit3, mask3
)

解答:

  • bit0 的值为 1,mask0 的值为 0
  • bit1 的值为 2,mask1 的值为 1
  • bit3 的值为 8,mask3 的值为 7

在常量声明语句中,iota 往往用于声明连续的整型常量。iota 的取值与其出现的位置强相关

关于 iota 的取值规则,有些书上或博客中可能给出类似下面的描述:

  • iota 在 const 关键字出现时被重置为 0
  • const 声明块中每新增一行,iota 值自增 1

这种描述本身没有错误,在简单的语句中套用这种规则可以快速地计算 iota 的取值,但在面对复杂的语句时这种规则往往充满歧义。
笔者也曾经这么理解 iota,但在阅读各种复杂源码时经常心生困惑而不知如何计算,究其原因是这种描述没有准确地描述 iota 的本意

实际上从编译器的角度看 iota,其取值规则只有一条

  • iota 代表了 const 声明块的行索引(下标从 0 开始)

这样理解更贴近编译器的实现逻辑,也更准确。
除此之外,const 声明还有一个特点,即如果为常量指定了一个表达式,但后续的常量没有表达式,则继承上面的表达式

接下来,我们根据这个规则来分析一下复杂的常量声明:

1
2
3
4
5
6
const (
    bit0, mask0 = 1 << iota, 1 << iota - 1  // const 声明第 0 行,即 iota == 0
    bit1, mask1  // const 声明第 1 行,即 iota == 1,表达式继承上面的语句
    _, _  // const 声明第 2 行,即 iota == 2
    bit3, mask3  // const 声明第 3 行,即 iota == 3
)
  • 第 0 行的表达式展开即 bit0, mask0 = 1 << 0, 1 << 0 - 1,所以 bit0 == 1, mask0 == 0
  • 第 1 行没有指定表达式继承第一行,即 bit1, mask1 = 1 << 1, 1 <<1 - 1,所以 bit1 == 2, mask1 == 1
  • 第 2 行没有定义常量
  • 第 3 行没有指定表达式继承第一行,即 bit3, mask3 = 1 << 3, 1 << 3 - 1,所以 bit3 = 8, mask3 == 7

itoa 实现原理

iota 标识符仅能用于常量声明语句中,它的取值与常量声明块中的代码的行数强相关,可以说它标识的正是常量声明语句中的行数(由 0 开始)。
那么它为什么会表现出这样的行为呢?

答案在于编译器处理常量声明语句的方式

在编译器代码中,每个常量声明语句使用 ValueSpec 结构表示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// A ValueSpec node represents a constant or variable declaration
// (ConstSpec or VarSpec production).

type ValueSpec struct {
    Doc *CommentGroup  // associated documentation; or nil
    Names []*Ident  // value names (len(Names) > 0)
    Type Expr  // value type; or nil
    Values []Expr  // initial values; or nil
    Comment *CommentGroup  // line comments; or nil
}

ValueSpec 结构不仅可以用来表示常量声明,还可以表示变量声明,不过它仅表示一种声明语句,比如:

1
2
3
4
const (
    // 常量的注释(文档)
    a, b = iota, iota // 常量的行注释
)

上面的常量声明块中仅包括一行声明语句,该语句对应一个 ValueSpec 结构

  • Doc 表示块注释,往往会出现在文档的注释中
  • Name 表示常量的名字,使用切片表示单行语句中声明的多个常量
  • Type 为常量类型
  • Value 为常量值,与 Name 对应,表示常量的初始值
  • Comment 表示行注释

编译器在构造常量时,实际上会遍历 ValueSpec 结构中的 Names 切片来逐个生成常量。
相关代码比较复杂,这里我们给出构造常量的伪算法,从中可以看出 iota 的作用

通常 const 语句块中会包含多行常量声明,那么就会对应多个 ValueSpce 结构,我们使用 ValueSpecs 表示多个 ValueSpec 结构,编译器构造常量的过程如下:

1
2
3
4
5
6
for iota, spec := range ValueSpecs {
    for i, name := range spce.Names {
        obj := NewConst(name, iota...)
        ...
    }
}

由上面的代码可以看出 iota 的本质,它仅代表常量声明的索引,所以它会表现出以下特征:

  • 单个 const 声明块中从 0 开始取值
  • 单个 const 声明块中,每增加一行声明,iota 的取值增 1,即便声明中没有使用 iota 也是如此
  • 单行声明语句中,即便出现多个 iota,iota 的取值也保持不变

内建变量类型

类型 长度 默认值 说明
bool 1 false
byte 1 0 uint8
int, uint 4,8 0 默认整数类型,依据目标平台,32 或 64 位
int8, uint8 1 0 -128~127, 0~255
int16, uint16 2 0 -32768~32767, 0~65535
int32,uint32 4 0 -21亿~21亿 0~42亿
int64,uint64 8 0
float32 4 0.0
float64 8 0.0 默认浮点数类型
complex64 8
complex128 16
rune 4 0 Unicode Code Point, int32
uintptr 4,8 0 足以存储指针的 uint
string "" 字符串,默认值位空字符串,而非 NULL
array 数组
struct 结构体
function nil 函数
interface nil 接口
map nil 字典,引用类型
slice nil 切片,引用类型
channel nil 通道,引用类型

bool, string
(u)int, (u)int8, (u)int16, (u)int32, (u)int64, uintptr(指针)
byte, rune
float32, float64, complex64, complex128

没有 char 只有 rune

支持八进制、十六进制以及科学记数法。
标准库 math 定义了各数字类型的取值范围.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "fmt"
    "math"
)

func main() {
    a, b, c := 100, 0144, 0x64

    fmt.Println(a, b, c)
    fmt.Printf("0b%b, %#o, %#x\n", a, a, a)

    fmt.Println(math.MinInt8, math.MaxInt8)
}

输出:

1
2
3
100 100 100
0b1100100, 0144, 0x64
-128 127

标准库 strconv 可在不同进制(字符串)间转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "strconv"
)

func main() {
    a, _ := strconv.ParseInt("1100100", 2, 32)
    b, _ := strconv.ParseInt("0144", 8, 32)
    c, _ := strconv.ParseInt("64", 16, 32)

    println(a, b, c)
    println("0b" + strconv.FormatInt(a, 2))
    println("0" + strconv.FormatInt(a, 8))
    println("0x" + strconv.FormatInt(a, 16))
}

输出:

1
2
3
4
100 100 100
0b1100100
0144
0x64

使用浮点数时,须注意小数位的有效精度,相关细节可参考 IEEE-754 标准

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func main() {
    var a float32 = 1.1234567899 // 注意: 默认浮点类型是 float64
    var b float32 = 1.12345678
    var c float32 = 1.123456781

    println(a, b, c)

    println(a == b, a == c)
    fmt.Printf("%v%v, %v\n", a, b, c)
}

输出:

1
2
3
+1.123457e+000 +1.123457e+000 +1.123457e+000
true true
1.12345681.1234568, 1.1234568
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "fmt"
    "math/cmplx"
)

func main() {
    a := 3 + 4i
    fmt.Println(cmplx.Abs(a))
}
// 5
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "math"
    "math/cmplx"
)

func euler() {
    fmt.Println(cmplx.Exp(1i*math.Pi)+1,
        cmplx.Pow(math.E, 1i*math.Pi)+1)
    fmt.Printf("%.3f\n", cmplx.Exp(1i*math.Pi)+1)
}

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

func main() {
    euler()
    triangle()
}
// (0+1.2246467991473515e-16i) (0+1.2246467991473515e-16i)
// (0.000+0.000i)
// 5

别名

在官方的语言规范中,专门提到两个别名

1
2
byte alias for uint8
rune alias for int32

别名类型无须转换,可直接赋值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

func test(x byte) {
    println(x)
}

func main() {
    var a byte = 0x11
    var b uint8 = a
    var c uint8 = a + b
    test(c)
}

但这并不表示,拥有相同底层结构的就属于别名。
就算在 64 位平台上 int 和 int64 结构完全一致,也分属不同类型,须显式转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func add(x, y int) int {
    return x + y
}

func main() {
    var x int = 100
    var y int64 = x // 错误: cannot use x(type int) as type int64 in assignment

    add(x, y)  // 错误: cannot use y(type int64) as type int in argument to add
}

整型

尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。
事实上,内置的len函数返回一个有符号的int,我们可以像下面例子那样处理逆序循环。

1
2
3
4
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数,那么i也将是无符号的uint类型,然后条件 i >= 0 则永远为真。
在三次迭代之后,也就是 i == 0 时,i-- 语句将不会产生 -1,而是变成一个uint类型的最大值,
然后 medals[i] 表达式将发生运行时panic异常,也就是试图访问一个slice范围以外的元素。

出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。

当使用fmt包打印一个数值时,我们可以用 %d%o%x 参数控制输出的进制格式,就像下面的例子:

1
2
3
4
5
6
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个 % 参数时将会包含对应相同数量的额外操作数,但是 % 之后的 [1] 副词告诉Printf函数再次使用第一个操作数。
第二,% 后的 # 副词告诉Printf在用 %o%x%X 输出时生成 00x0X 前缀。

字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似 'a' 写法的字符面值,但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符,马上将会看到这样的例子。

字符使用%c参数打印,或者是用%q参数打印带单引号的字符:

1
2
3
4
5
6
ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii)   // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
fmt.Printf("%d %[1]q\n", newline)       // "10 '\n'"

浮点数

Go语言提供了两种精度的浮点数,float32和float64。它们的算术规范由IEEE754浮点数国际标准定义,该浮点数规范被所有现代的CPU支持。

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。
常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38;对应的 math.MaxFloat64 常量大约是 1.8e308
它们分别能表示的最小值近似为 1.4e-454.9e-324

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;
通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大
(因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差):

1
2
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

用Printf函数的 %g 参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用 %e(带指数)或 %f 的形式打印可能更合适。
所有的这三个打印形式都可以指定打印的宽度和控制打印精度。

1
2
3
for x := 0; x < 8; x++ {
    fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}

上面代码打印e的幂,打印精度是小数点后三个小数精度和8个字符宽度:

1
2
3
4
5
6
7
8
x = 0       e^x =    1.000
x = 1       e^x =    2.718
x = 2       e^x =    7.389
x = 3       e^x =   20.086
x = 4       e^x =   54.598
x = 5       e^x =  148.413
x = 6       e^x =  403.429
x = 7       e^x = 1096.633

math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;
还有NaN非数,一般用于表示无效的除法操作结果 0/0Sqrt(-1).

1
2
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"

函数 math.IsNaN 用于测试一个数是否是非数 NaN,math.NaN 则返回非数对应的值。
虽然可以用 math.NaN 来表示一个非法的结果,但是测试一个结果是否是非数 NaN 则是充满风险的,因为NaN和任何数都是不相等的(在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示):

1
2
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:

1
2
3
4
5
6
7
func compute() (value float64, ok bool) {
    // ...
    if failed {
        return 0, false
    }
    return result, true
}

引用类型

所谓引用类型特指 slice、map、channel 这三种预定义类型

相比数字、数组等类型,引用类型拥有更复杂的存储结构。
除分配内存外,它们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。

内置函数 new 按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。
而引用类型则必须使用 make 函数创建,编译器会将 make 转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

func mkslice() []int {
    s := make([]int, 0, 10)
    s = append(s, 100)
    return s
}

func mkmap() map[string]int {
    m := make(map[string]int)
    m["a"] = 1
    return m
}

func main() {
    m := mkmap()
    println(m["a"])

    s := mkslice()
    println(s[0])
}

除 new/make 函数外,也可使用初始化表达式,编译器生成的指令基本相同

当然,new 函数也可为引用类型分配内存,但这是不完整创建。
以字典(map)为例,它仅为分配了字典类型本身(实际就是个指针包装)所需内存,并没有分配键值存储内存,也没有初始化散列桶等内部属性,因此它无法正常工作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    p := new(map[string]int) // 函数 new 返回指针
    m := *p
    m["a"] = 1 // panic:assignment to entry in nil map(运行期错误)
    fmt.Println(m)
}

类型转换

隐式转换造成的问题远大于它带来的好处。

除常量、别名类型以及未命名类型外,Go 强制要求使用显式类型转换。
加上不支持操作符重载,所以我们总是能确定语句及表达式的明确含义。

1
2
3
a := 10
b := byte(a)
c := a + int(b)  // 混合类型表达式必须确保类型一致

同样不能将非 bool 类型结果当作 ture/false 使用

1
2
3
4
5
6
7
func main() {
    x := 100
    var b bool = x // 错误: cannot use x(type int) as type bool in assignment

    if x {  // 错误: non-bool x(type int)used as if condition
    }
}

语法歧义

如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以避免造成语法分解错误。

1
2
3
4
5
6
func main() {
    x := 100
    p := *int(&x)  // 错误: cannot convert &x(type *int) to type int
                   // invalid indirect of int(&x) (type int)
    println(p)
}

正确的做法是用括号,让编译器将*int解析为指针类型

1
2
3
4
5
6
7
(*int)(p) -> 如果没有括号 --> *(int(p))

(<-chan int)(c) <-(chan int(c))
(func())(x) func()x

func() int (x) -> 有返回值的函数类型可省略括号但仍然建议使用
(func() int) (x) 使用括号后更易阅读

自定义类型

使用关键字 type 定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type flags byte

const (
    read flags = 1 << iota
    write
    exec
)

func main() {
    f := read | exec
    fmt.Printf("%b\n", f)   // 输出二进制标记位
}

输出:

1
101

和 var、const 类似,多个 type 定义可合并成组,可在函数或代码块内定义局部类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
    type(  // 组
        user struct {  // 结构体
            name string
            age uint8
        }

        event func(string) bool  // 函数类型
    )

    u := user{"Tom", 20}
    fmt.Println(u)

    var f event = func(s string) bool {
        println(s)
        return s != ""
    }

    f("abc")
}

即便指定了基础类型,也只表明它们有相同底层数据结构,两者间不存在任何关系,属完全不同的两种类型。
除操作符外,自定义类型不会继承基础类型的其他信息(包括方法)。
不能视作别名,不能隐式转换,不能直接用于比较表达式

1
2
3
4
5
6
7
8
9
func main() {
    type data int
    var d data = 10

    var x int = d  // 错误: cannot use d(type data) as type int in assignment
    println(x)

    println(d == x) // 错误: invalid operation: d == x(mismatched types data and int)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv

import "fmt"

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

我们在这个包声明了两种类型:Celsius和Fahrenheit分别对应不同的温度单位。
它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。
刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。
Celsius(t)Fahrenheit(t) 是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。
另一方面,CToF和FToC两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

对于每一个类型T,都有一个对应的类型转换操作 T(x),用于将x转为T类型(如果T是指针类型,可能会需要用小括弧包装 T,比如 (*int)(0))。
只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。
如果x是可以赋值给T类型的值,那么x必然也可以被转为T类型,但是一般没有这个必要。

底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。
这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的,正如我们所期望的那样。

1
2
3
4
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch

比较运算符 ==< 也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。
但是如果两个值有着不同的类型,则不能直接进行比较:

1
2
3
4
5
6
var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

注意最后那个语句。尽管看起来像函数调用,但是 Celsius(f) 是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。
测试为真的原因是因为c和g都是零值。

一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(例如用匿名的结构体定义变量)。
虽然对于像float64这种简单的底层类型没有简洁很多,但是如果是复杂的类型将会简洁很多,特别是结构体类型。

下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个叫名叫String的方法,该方法返回该类型对象c带着 °C 温度单位的字符串:

1
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印

1
2
3
4
5
6
7
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String

未命名类型

与有明确标识符的 bool、int、string 等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等属性有关,故称作未命名类型。
当然,可用 type 为其提供具体名称,将其改编为命名类型。

具有相同声明的未命名类型被视作同一类型

  • 具有相同基类型的指针
  • 具有相同元素类型和长度的数组(array)
  • 具有相同元素类型的切片(slice)
  • 具有相同键值类型的字典(map)
  • 具有相同数据类型及操作方法的通道(channel)
  • 具有相同字段序列(字段名、字段类型、标签,以及字段顺序)的结构体(struct)
  • 具有相同签名(参数和返回值列表,不包括参数名)的函数(func)
  • 具有相同方法集(方法名、方法签名,不补哦阔顺序)的接口(interface)

容易被忽略的是 struct tag,它也属于类型组成部分,而不仅仅是元数据描述

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    var a struct {  // 匿名结构类型
        x int `x`
        s string `s`
    }

    var b struct {
        x int
        s string
    }

    b = a // 错误: cannot use a type struct{x int "x"; s string "s"} as type
          // struct{x int; s string} in assignment
    fmt.Println(b)
}

同样,函数的参数顺序也属签名组成部分

1
2
3
4
5
6
7
func main() {
    var a func(int, string)
    var b func(string, int)

    b = a // 错误: cannot use a(type func(int, string)) as type
          // func(string, int) in assignment b("s", 1)
}

未命名类型转换规则:

  • 所属类型相同
  • 基础类型相同,且其中一个是未命名类型
  • 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型
  • 将默认值 nil 赋值给切片、字典、通道、指针、函数或接口
  • 对象实现了目标接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    type data[2] int
    var d data = [2]int{1, 2}  // 基础类型相同,右值为未命名类型    

    fmt.Println(d)

    a := make(chan int, 2)
    var b chan <-int = a // 双向通道转换为单向通道,其中 b 为未命名乐境

    b <- 2
}

条件语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
    "fmt"
    "io/ioutil"
)

func bounded(v int) int {
    if v > 100 {
        return 100
    } else if v < 0 {
        return 0
    } else {
        return v
    }
}

func main() {
    fmt.Println(bounded(100))
    const filename = "abc.txt"
    // contents, err := ioutil.ReadFile(filename)
    // if err != nil {
    //  fmt.Println(err)
    // } else {
    //  fmt.Printf("%s\n", contents)
    // }
    if contents, err := ioutil.ReadFile(filename); err != nil {
        fmt.Println(err)
    } else {
        fmt.Printf("%s\n", contents)
    }
}
// 100
// 1 2 3
// hhh

if 的条件里可以赋值
if 的条件里赋值的变量作用域就在这个 if 语句里

函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package main

import (
    "fmt"
    "math"
    "reflect"
    "runtime"
)

func eval(a, b int, op string) (int, error) {
    switch op {
    case "+":
        return a + b, nil
    case "-":
        return a - b, nil
    case "*":
        return a * b, nil
    case "/":
        q, _ := div(a, b)
        return q, nil
    default:
        return 0, fmt.Errorf(
            "unsupported operation: %s", op)
    }
}

func div(a, b int) (q, r int) {
    // return a / b, a % b
    q = a / b
    r = a % b
    return
}

func apply(op func(int, int) int, a, b int) int {
    p := reflect.ValueOf(op).Pointer()
    opName := runtime.FuncForPC(p).Name()
    fmt.Printf("Calling function %s with args "+
        "(%d %d)\n", opName, a, b)
    return op(a, b)
}

func pow(a, b int) int {
    return int(math.Pow(float64(a), float64(b)))
}

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

func main() {
    fmt.Println(eval(3, 4, "/"))
    a, b := div(13, 4)
    fmt.Println(a, b)
    if result, err := eval(3, 4, "h"); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
    fmt.Println(apply(pow, 4, 3))
    fmt.Println(apply(func(a, b int) int {
        return int(math.Pow(float64(a), float64(b)))
    }, 3, 4))
    fmt.Println(1, 2, 3, 4, 5)
}
// 0 <nil>
// 3 1
// unsupported operation: h
// Calling function main.pow with args (4 3)
// 64
// Calling function main.main.func1 with args (3 4)
// 81
// 1 2 3 4 5

指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    var a int = 2
    var pa *int = &a
    *pa = 3
    fmt.Println(a)
}
// 3

指针不能运算

Go 语言只有值传递一种方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

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

func main() {
    a, b := 3, 4
    // swap(&a, &b)
    a, b = swap(a, b)
    fmt.Println(a, b)
}
// 4 3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func zeroval(ival int) {
    ival = 0
}

func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("initial:", i)

    zeroval(i)
    fmt.Println("zeroval:", i)

    zeroptr(&i)
    fmt.Println("zeroptr:", i)

    fmt.Println("pointer:", &i)
}

输出结果:

1
2
3
4
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0xc00002a0f8

随机数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().UnixNano())
    fmt.Println(rand.Intn(100))
}