• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

Go学习笔记01-语言

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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 字节数组。

  • 默认值是空字符串 ""。
  • 用索引号访问某字节,如 s[i]。
  • 不能用序号获取字节元素指针,&s[i] 非法。
  • 不可变类型,无法修改字节数组。
  • 字节数组尾部不包含 NULL。
  • runtime.h

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。

  • 默认值 nil,没有 NULL 常量。
  • 操作符 "&" 取变量地址,"*" 透过指针访问目标对象。
  • 不支持指针运算,不支持 "->" 运算符,直接用 "." 访问目标成员。
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 等和具体元素类型、长度等有关,属于未命名类型。

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

  • 具有相同基类型的指针。
  • 具有相同元素类型和长度的 array。
  • 具有相同元素类型的 slice。
  • 具有相同键值类型的 map。
  • 具有相同元素类型和传送方向的 channel。
  • 具有相同字段序列 (字段名、类型、标签、顺序) 的匿名 struct。
  • 签名相同 (参数和返回值,不包括参数名称) 的 function。
  • 方法集相同 (方法名、方法签名相同,和次序无关) 的 interface。
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

和以往认知的数组有很大不同。

  • 数组是值类型,赋值和传参会复制整个数组,而不是指针。
  • 数组长度必须是常量,且是类型的组成部分。[2]int 和 [3]int 是不同类型。
  • 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
  • 指针数组 [n]T,数组指针 [n]T。

可用复合语句初始化。

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,或数组指针。

 
                       
                    
                    

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
vscode 配置golang go开发环境 IDE(2018年10月)发布时间:2022-07-10
下一篇:
go语言内存管理(1)发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap