在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
1.1 变量Go 是静态类型语言,不能在运行期改变变量类型。使用关键字 var 定义变量,自动初始化为零值。如果提供初始化值,可省略变量类型,由编译器自动推断。 var x int
var f float32 = 1.6
var s = "abc"
在函数内部,可用更简略的 ":=" 方式定义变量。 func main() {
x := 123 // 注意检查,是定义新局部变量,还是修改全局变量。该方式容易造成错误。
}
可一次定义多个变量。 var x, y, z int
var s, n = "abc", 123
var (
a int
b float32
)
func main() {
n, s := 0x1234, "Hello, World!"
println(x, s, n)
}
多变量赋值时,先计算所有相关值,然后再从左到右依次赋值。 data, i := [3]int{0, 1, 2}, 0
i, data[i] = 2, 100 // (i = 0) -> (i = 2), (data[0] = 100)
特殊只写变量 "_",用于忽略值占位。 func test() (int, string) {
return 1, "abc"
}
func main() {
_, s := test()
println(s)
}
编译器会将未使用的局部变量当做错误。 var s string // 全局变量没问题。
func main() {
i := 0 // Error: i declared and not used。(可使用 "_ = i" 规避)
}
注意重新赋值与定义新同名变量的区别。 s := "abc"
println(&s)
s, y := "hello", 20 // 重新赋值: 与前 s 在同一层次的代码块中,且有新的变量被定义。
println(&s, y) // 通常函数多返回值 err 会被重复使用。
{
s, z := 1000, 30 // 定义新同名变量: 不在同一层次代码块。
println(&s, z)
}
输出: 0x2210230f30 0x2210230f30 20 0x2210230f18 30 1.2 常量常量值必须是编译期可确定的数字、字符串、布尔值。 const x, y int = 1, 2 // 多常量初始化
const s = "Hello, World!" // 类型推断
const ( // 常量组
a, b = 10, 100
c bool = false
)
func main() {
const x = "xxx" // 未使用局部常量不会引发编译错误。
}
不支持 1UL、2LL 这样的类型后缀。 在常量组中,如不提供类型和初始化值,那么视作与上一常量相同。 const (
s = "abc"
x // x = "abc"
)
常量值还可以是 len、cap、unsafe.Sizeof 等编译期可确定结果的函数返回值。 const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(b)
)
如果常量类型足以存储初始化值,那么不会引发溢出错误。 const (
a byte = 100 // int to byte
b int = 1e20 // float64 to int, overflows
)
枚举 关键字 iota 定义常量组中从0开始按行计数的自增枚举值。 const (
Sunday = iota // 0
Monday // 1,通常省略后续行表达式。
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
const (
_ = iota // iota = 0
KB int64 = 1 << (10 * iota) // iota = 1
MB // 与 KB 表达式相同,但 iota = 2
GB
TB
)
在同一常量组中,可以提供多个 iota,它们各自增长。 const (
A, B = iota, iota << 10 // 0, 0 << 10
C, D // 1, 1 << 10
)
如果 iota 自增被打断,须显式恢复。 const (
A = iota // 0
B // 1
C = "c" // c
D // c,与上一行相同。
E = iota // 4,显式恢复。注意计数包含了 C、D 两行。
F // 5
)
可通过自定义类型来实现枚举类型限制。 type Color int
const (
Black Color = iota
Red
Blue
)
func test(c Color) {}
func main() {
c := Black
test(c)
x := 1
test(x) // Error: cannot use x (type int) as type Color in function argument
test(1) // 常量会被编译器自动转换。
}
1.3 基本类型更明确的数字类型命名,支持 Unicode,支持常用数据结构。 支持八进制、十六进制,以及科学记数法。标准库 math 定义了各数字类型取值范围。 a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
空指针值 nil,而非 C/C++ NULL。 1.4 引用类型引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。 内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。 a := []int{0, 0, 0} // 提供初始化表达式。
a[1] = 10
b := make([]int, 3) // makeslice
b[1] = 10
c := new([]int)
c[1] = 10 // Error: invalid operation: c[1] (index of type *[]int)
有关引用类型具体的内存布局,可参考后续章节。 1.5 类型转换不支持隐式类型转换,即便是从窄向宽转换也不行。 var b byte = 100
// var n int = b // Error: cannot use b (type byte) as type int in assignment
var n int = int(b) // 显式转换
使用括号避免优先级错误。 *Point(p) // 相当于 *(Point(p))
(*Point)(p)
<-chan int(c) // 相当于 <-(chan int(c))
(<-chan int)(c)
同样不能将其他类型当 bool 值使用。 a := 100
if a { // Error: non-bool a (type int) used as if condition
println("true")
}
1.6 字符串字符串是不可变值类型,内部用指针指向 UTF-8 字节数组。
struct String
{
byte* str;
intgo len;
};
使用索引号访问字符 (byte)。 s := "abc"
println(s[0] == '\x61', s[1] == 'b', s[2] == 0x63)
输出: true true true
使用 "`" 定义不做转义处理的原始字符串,支持跨行。 s := `a
b\r\n\x00
c`
println(s)
输出: a
b\r\n\x00
c
连接跨行字符串时,"+" 必须在上一行末尾,否则导致编译错误。 s := "Hello, " +
"World!"
s2 := "Hello, "
+ "World!" // Error: invalid operation: + untyped string
支持用两个索引号返回子串。子串依然指向原字节数组,仅修改了指针和长度属性。 s := "Hello, World!"
s1 := s[:5] // Hello
s2 := s[7:] // World!
s3 := s[1:5] // ello
单引号字符常量表示 Unicode Code Point,支持 \uFFFF、\U7FFFFFFF、\xFF 格式。对应 rune 类型,UCS-4。 func main() {
fmt.Printf("%T\n", 'a')
var c1, c2 rune = '\u6211', '们'
println(c1 == '我', string(c2) == "\xe4\xbb\xac")
}
输出: int32 // rune 是 int32 的别名
true true
要修改字符串,可先将其转换成 []rune 或 []byte,完成后再转换为 string。无论哪种转换,都会重新分配内存,并复制字节数组。 func main() {
s := "abcd"
bs := []byte(s)
bs[1] = 'B'
println(string(bs))
u := "电脑"
us := []rune(u)
us[1] = '话'
println(string(us))
}
输出: aBcd
电话
用 for 循环遍历字符串时,也有 byte 和 rune 两种方式。 func main() {
s := "abc汉字"
for i := 0; i < len(s); i++ { // byte
fmt.Printf("%c,", s[i])
}
fmt.Println()
for _, r := range s { // rune
fmt.Printf("%c,", r)
}
}
输出: a,b,c,,±,,,,, a,b,c,汉,字, 1.7 指针支持指针类型 *T,指针的指针 *T,以及包含包名前缀的 .T。
func main() {
type data struct{ a int }
var d = data{1234}
var p *data
p = &d
fmt.Printf("%p, %v\n", p, p.a) // 直接用指针访问目标对象成员,无须转换。
}
输出: 0x2101ef018, 1234 不能对指针做加减法等运算。 x := 1234
p := &x
p++ // Error: invalid operation: p += 1 (mismatched types *int and int)
可以在 unsafe.Pointer 和任意类型指针间进行转换。 func main() {
x := 0x12345678
p := unsafe.Pointer(&x) // *int -> Pointer
n := (*[4]byte)(p) // Pointer -> *[4]byte
for i := 0; i < len(n); i++ {
fmt.Printf("%X ", n[i])
}
}
输出: 78 56 34 12 返回局部变量指针是安全的,编译器会根据需要将其分配在 GC Heap 上。 func test() *int {
x := 100
return &x // 在堆上分配 x 内存。但在内联时,也可能直接分配在目标栈。
}
将 Pointer 转换成 uintptr,可变相实现指针运算。 func main() {
d := struct {
s string
x int
}{"abc", 100}
p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
p += unsafe.Offsetof(d.x) // uintptr + offset
p2 := unsafe.Pointer(p) // uintptr -> Pointer
px := (*int)(p2) // Pointer -> *int
*px = 200 // d.x = 200
fmt.Printf("%#v\n", d)
}
输出: struct { s string; x int }{s:"abc", x:200}
注意:GC 把 uintptr 当成普通整数对象,它无法阻止 "关联" 对象被回收。 1.8 自定义类型可将类型分为命名和未命名两大类。命名类型包括 bool、int、string 等,而 array、slice、map 等和具体元素类型、长度等有关,属于未命名类型。 具有相同声明的未命名类型被视为同一类型。
var a struct { x int `a` }
var b struct { x int `ab` }
// cannot use a (type struct { x int "a" }) as type struct { x int "ab" } in assignment
b = a
可用 type 在全局或函数内定义新类型。 func main() {
type bigint int64
var x bigint = 100
println(x)
}
新类型不是原类型的别名,除拥有相同数据存储结构外,它们之间没有任何关系,不会持有原类型任何信息。除非目标类型是未命名类型,否则必须显式转换。 x := 1234
var b bigint = bigint(x) // 必须显式转换,除非是常量。
var b2 int64 = int64(b)
var s myslice = []int{1, 2, 3} // 未命名类型,隐式转换。
var s2 []int = s
2.1 保留字语言设计简练,保留字不多。 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
2.2 运算符全部运算符、分隔符,以及其他符号。 运算符结合律全部从左到右。 简单位运算演示。 0110 & 1011 = 0010 AND 都为1。
0110 | 1011 = 1111 OR 至少一个为1。
0110 ^ 1011 = 1101 XOR 只能一个为1。
0110 &^ 1011 = 0100 AND NOT 清除标志位。
标志位操作。 a := 0
a |= 1 << 2 // 0000100: 在 bit2 设置标志位。
a |= 1 << 6 // 1000100: 在 bit6 设置标志位
a = a &^ (1 << 6) // 0000100: 清除 bit6 标志位。
不支持运算符重载。尤其需要注意,"++"、"--" 是语句而非表达式。 n := 0
p := &n
// b := n++ // syntax error
// if n++ == 1 {} // syntax error
// ++n // syntax error
n++
*p++ // (*p)++
没有 "~",取反运算也用 "^"。 x := 1
x, ^x // 0001, -0010
2.3 初始化初始化复合对象,必须使用类型标签,且左大括号必须在类型尾部。 // var a struct { x int } = { 100 } // syntax error
// var b []int = { 1, 2, 3 } // syntax error
// c := struct {x int; y string} // syntax error: unexpected semicolon or newline
// {
// }
var a = struct{ x int }{100}
var b = []int{1, 2, 3}
初始化值以 "," 分隔。可以分多行,但最后一行必须以 "," 或 "}" 结尾。 a := []int{
1,
2 // Error: need trailing comma before newline in composite literal
}
a := []int{
1,
2, // ok
}
b := []int{
1,
2 } // ok
2.4 控制流2.4.1 IF很特别的写法:
x := 0
// if x > 10 // Error: missing condition in if statement
// {
// }
if n := "abc"; x > 0 { // 初始化语句未必就是定义变量,比如 println("init") 也是可以的。
println(n[2])
} else if x < 0 { // 注意 else if 和 else 左大括号位置。
println(n[1])
} else {
println(n[0])
}
不支持三元操作符 "a > b a : b"。 2.4.2 For支持三种循环方式,包括类 while 语法。 s := "abc"
for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
println(s[i])
}
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}
for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}
不要期望编译器能理解你的想法,在初始化语句中计算出全部结果是个好主意。 func length(s string) int {
println("call length.")
return len(s)
}
func main() {
s := "abcd"
for i, n := 0, length(s); i < n; i++ { // 避免多次调用 length 函数。
println(i, s[i])
}
}
输出: call length.
0 97
1 98
2 99
3 100
2.4.3 Range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。 可忽略不想要的返回值,或用 "_" 这个特殊变量。 s := "abc"
for i := range s { // 忽略 2nd value,支持 string/array/slice/map。
println(s[i])
}
for _, c := range s { // 忽略 index。
println(c)
}
for range s { // 忽略全部返回值,仅迭代。
...
}
m := map[string]int{"a": 1, "b": 2}
for k, v := range m { // 返回 (key, value)。
println(k, v)
}
注意,range 会复制对象。 a := [3]int{0, 1, 2}
for i, v := range a { // index、value 都是从复制品中取出。
if i == 0 { // 在修改前,我们先修改原数组。
a[1], a[2] = 999, 999
fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
}
a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。
}
fmt.Println(a) // 输出 [100, 101, 102]。
建议改用引用类型,其底层数据不会被复制。 s := []int{1, 2, 3, 4, 5}
for i, v := range s { // 复制 struct slice { pointer, len, cap }。
if i == 0 {
s = s[:3] // 对 slice 的修改,不会影响 range。
s[2] = 100 // 对底层数据的修改。
}
println(i, v)
}
输出: 0 1 1 2 2 100 3 4 4 5 另外两种引用类型 map、channel 是指针包装,而不像 slice 是 struct。 2.4.4 Switch分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。 x := []int{1, 2, 3}
i := 2
switch i {
case x[1]:
println("a")
case 1, 3:
println("b")
default:
println("c")
}
输出: a 如需要继续下一分支,可使用 fallthrough,但不再判断条件。 x := 10
switch x {
case 10:
println("a")
fallthrough
case 0:
println("b")
}
输出: a
b
省略条件表达式,可当 if...else if...else 使用。 switch {
case x[1] > 0:
println("a")
case x[1] < 0:
println("b")
default:
println("c")
}
switch i := x[2]; { // 带初始化语句
case i > 0:
println("a")
case i < 0:
println("b")
default:
println("c")
}
2.4.5 Goto, Break, Continue支持在函数内 goto 跳转。标签名区分大小写,未使用标签引发错误。 func main() {
var i int
for {
println(i)
i++
if i > 2 { goto BREAK }
}
BREAK:
println("break")
EXIT: // Error: label EXIT defined and not used
}
配合标签,break 和 continue 可在多级嵌套循环中跳出。 func main() {
L1:
for x := 0; x < 3; x++ {
L2:
for y := 0; y < 5; y++ {
if y > 2 { continue L2 }
if x > 1 { break L1 }
print(x, ":", y, " ")
}
println()
}
}
输出: 0:0 0:1 0:2
1:0 1:1 1:2
附:break 可用于 for、switch、select,而 continue 仅能用于 for 循环。 x := 100
switch {
case x >= 0:
if x == 0 { break }
println(x)
}
第3章 函数3.1 函数定义不支持 嵌套 (nested)、重载 (overload) 和 默认参数 (default parameter)。
使用关键字 func 定义函数,左大括号依旧不能另起一行。 func test(x, y int, s string) (int, string) { // 类型相同的相邻参数可合并。
n := x + y // 多返回值必须用括号。
return n, fmt.Sprintf(s, n)
}
函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。 func test(fn func() int) int {
return fn()
}
type FormatFunc func(s string, x, y int) string // 定义函数类型。
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1, s2)
}
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。 3.2 变参变参本质上就是 slice。只能有一个,且必须是最后一个。 func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
println(test("sum: %d", 1, 2, 3))
}
使用 slice 对象做变参时,必须展开。 func main() {
s := []int{1, 2, 3}
println(test("sum: %d", s...))
}
3.3 返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。 func test() (int, int) {
return 1, 2
}
func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context
x, _ := test()
println(x)
}
多返回值可直接作为其他函数调用实参。 func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。 func add(x, y int) (z int) {
z = x + y
return
}
func main() {
println(add(1, 2))
}
命名返回参数可被同名局部变量遮蔽,此时需要显式返回。 func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}
命名返回参数允许 defer 延迟调用通过闭包读取和修改。 func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2)) // 输出: 103
}
显式 return 返回前,会先修改命名返回参数。 func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}
func main() {
println(add(1, 2)) // 输出: 203
}
3.4 匿名函数匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。 // --- function variable ---
fn := func() { println("Hello, World!") }
fn()
// --- function collection ---
fns := [](func(x int) int){
func(x int) int { return x + 1 },
func(x int) int { return x + 2 },
}
println(fns[0](100))
// --- function as field ---
d := struct {
fn func() string
}{
fn: func() string { return "Hello, World!" },
}
println(d.fn())
// --- channel of function ---
fc := make(chan func() string, 2)
fc <- func() string { return "Hello, World!" }
println((<-fc)())
闭包复制的是原对象指针,这就很容易解释延迟引用现象。 func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
f := test()
f()
}
输出: x (0x2101ef018) = 100
x (0x2101ef018) = 100
在汇编层面,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调用匿名函数时,只需以某个寄存器传递该对象即可。 FuncVal { func_address, closure_var_pointer ... }
3.5 延迟调用关键字 defer 用于注册延迟调用。这些调用直到 ret 前才被执行,通常用于释放资源或错误处理。 func test() error {
f, err := os.Create("test.txt")
if err != nil { return err }
defer f.Close() // 注册调用,而不是注册函数。必须提供参数,哪怕为空。
f.WriteString("Hello, World!")
return nil
}
多个 defer 注册,按 FILO 次序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。 func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}
输出: c
b
a
panic: runtime error: integer divide by zero
延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。 func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引用
}(x) // x 被复制
x += 10
y += 100
println("x =", x, "y =", y)
}
输出: x = 20 y = 120
defer: 10 120
滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。 var lock sync.Mutex
func test() {
lock.Lock()
lock.Unlock()
}
func testdefer() {
lock.Lock()
defer lock.Unlock()
}
func BenchmarkTest(b *testing.B) {
for i := 0; i < b.N; i++ {
test()
}
}
func BenchmarkTestDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
testdefer()
}
}
输出: BenchmarkTest" 50000000 43 ns/op BenchmarkTestDefer 20000000 128 ns/op 3.6 错误处理没有结构化异常,使用 panic 抛出错误,recover 捕获错误。 func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
}
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。 func panic(v interface{})
func recover() interface{}
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。 func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
输出: defer panic
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。 func test() {
defer recover() // 无效!
defer fmt.Println(recover()) // 无效!
defer func() {
func() {
println("defer inner")
recover() // 无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
输出: defer inner
<nil>
panic: test panic
使用延迟匿名函数或下面这样都是有效的。 func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。 func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil { z = 0 }
}()
z = x / y
return
}()
println("x / y =", z)
}
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。 type error interface {
Error() string
}
标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。 var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 { return 0, ErrDivByZero }
return x / y, nil
}
func main() {
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
如何区别使用 panic 和 error 两种方式?惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。 第4章 数据4.1 Array和以往认知的数组有很大不同。
可用复合语句初始化。 a := [3]int{1, 2} // 未初始化元素值为0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4:200} // 使用索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
支持多维数组。 a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
|
请发表评论