golang基础语法-1

栏目: Go · 发布时间: 5年前

内容简介:文章篇幅很长,包含golang大部分的基础内容。Go 是静态类型语言,不能在运行期改变变量类型。使用关键字 var 定义变量,自动初始化为零值。如果提供初始化值,可省略变量类型,由编译器自动推断。在函数内部,可用更简略的 ":=" 方式定义变量。

文章篇幅很长,包含golang大部分的基础内容。

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,支持常用数据结构。

golang基础语法-1

image

支持八进制、十六进制,以及科学记数法。标准库 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])
    }
}

输出:

返回局部变量指针是安全的,编译器会根据需要将其分配在 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章 表达式

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 运算符

全部运算符、分隔符,以及其他符号。

golang基础语法-1

image

运算符结合律全部从左到右。

golang基础语法-1

image

简单位运算演示。

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

类似迭代器操作,返回 (索引, 值) 或 (键, 值)。

golang基础语法-1

image

可忽略不想要的返回值,或用 _ 这个特殊变量。

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)
}

输出:

另外两种引用类型 map、channel 是指针包装,而不像 slicestruct

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()
    }
}

输出:

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

func test(x [2]int) {
    fmt.Printf("x: %p\n", &x)
    x[1] = 1000
}

func main() {
    a := [2]int{}
    fmt.Printf("a: %p\n", &a)
    test(a)
    fmt.Println(a)
}

输出:

a: 0x2101f9150
x: 0x2101f9170
[0 0]

内置函数 len 和 cap 都返回数组长度 (元素数量)。

a := [2]int{}
println(len(a), cap(a)) // 2, 2

4.2 Slice

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

runtime.h

struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
  • 引用类型。但自身是结构体,值拷贝传递。
  • 属性 len 表示可用元素数量,读写操作不能超过该限制。
  • 属性 cap 表示最大扩张容量,不能超出数组限制。
  • 如果 slice == nil ,那么 len、cap 结果都等于 0。
data := [...]int{0, 1, 2, 3, 4, 5, 6}
slice := data[1:4:5] // [low : high : max]
golang基础语法-1

image

创建表达式使用的是元素索引号,而非数量。

golang基础语法-1

image

读写操作实际目标是底层数组,只需注意索引号的差别。

data := [...]int{0, 1, 2, 3, 4, 5}

s := data[2:4]
s[0] += 100
s[1] += 200

fmt.Println(s)
fmt.Println(data)

输出:

[102 203]
[0 1 102 203 4 5]

可直接创建 slice 对象,自动分配底层数组。

s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
fmt.Println(s1, len(s1), cap(s1))

s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))

s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
fmt.Println(s3, len(s3), cap(s3))

输出:

[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6

使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。

s := []int{0, 1, 2, 3}
p := &s[2] // *int, 获取底层数组元素指针。
*p += 100

fmt.Println(s)

输出:

[0 1 102 3]

至于 [][]T,是指元素类型为 []T 。

data := [][]int{
    []int{1, 2, 3},
    []int{100, 200},
    []int{11, 22, 33, 44},
}

可直接修改 struct array/slice 成员。

d := [5]struct {
    x int
}{}

s := d[:]

d[1].x = 10
s[2].x = 20

fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])

输出:

[{0} {10} {20} {0} {0}]
0x20819c180, 0x20819c180

4.2.1 reslice

所谓 reslice,是基于已有 slice 创建新 slice 对象,以便在 cap 允许范围内调整属性。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5] // [2 3 4]
s2 := s1[2:6:7] // [4 5 6 7]
s3 := s2[3:6] // Error
golang基础语法-1

image

新对象依旧指向原底层数组。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s1 := s[2:5] // [2 3 4]
s1[2] = 100

s2 := s1[2:6] // [100 5 6 7]
s2[3] = 200

fmt.Println(s)

输出:

[0 1 2 3 100 5 6 200 8 9]

4.2.2 append

向 slice 尾部添加数据,返回新的 slice 对象。

s := make([]int, 0, 5)
fmt.Printf("%p\n", &s)

s2 := append(s, 1)
fmt.Printf("%p\n", &s2)

fmt.Println(s, s2)

输出:

0x210230000
0x210230040
[] [1]

简单点说,就是在 array[slice.high] 写数据。

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[:3]
s2 := append(s, 100, 200) // 添加多个值。

fmt.Println(data)
fmt.Println(s)
fmt.Println(s2)

输出:

[0 1 2 100 200 5 6 7 8 9]
[0 1 2]
[0 1 2 100 200]

一旦超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。

data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]

s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

输出:

[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0x20819c180 0x20817c0c0

从输出结果可以看出, append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。

通常以2倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

s := make([]int, 0, 1)
c := cap(s)

for i := 0; i < 50; i++ {
    s = append(s, i)
    if n := cap(s); n > c {
        fmt.Printf("cap: %d -> %d\n", c, n)
        c = n
    }
}

输出:

cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64

4.2.3 copy

函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

s := data[8:]
s2 := data[:5]

copy(s2, s) // dst:s2, src:s

fmt.Println(s2)
fmt.Println(data)

输出:

[8 9 2 3 4]
[8 9 2 3 4 5 6 7 8 9]

应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

4.3 Map

引用类型,哈希表。键必须是支持相等运算符 (==、!=) 类型,比如 number、string、pointer、array、struct ,以及对应的 interface 。值可以是任意类型,没有限制。

m := map[int]struct {
    name string
    age int
}{
    1: {"user1", 10}, // 可省略元素类型。
    2: {"user2", 20},
}

println(m[1].name)

预先给 make 函数一个合理元素数量参数,有助于提升性能。因为事先申请一大块内存,可避免后续操作时频繁扩张。

m := make(map[string]int, 1000)

常见操作:

m := map[string]int{
    "a": 1,
}

if v, ok := m["a"]; ok { // 判断 key 是否存在。
    println(v)
}

println(m["c"]) // 对于不存在的 key,直接返回 \0,不会出错。

m["b"] = 2 // 新增或修改。

delete(m, "c") // 删除。如果 key 不存在,不会出错。

println(len(m)) // 获取键值对数量。cap 无效。

for k, v := range m { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。
    println(k, v)
}

不能保证迭代返回次序,通常是随机结果,具体和版本实现有关。

从 map 中取回的是一个 value 临时复制品,对其成员的修改是没有任何意义的。

type user struct{ name string }

m := map[int]user{ // 当 map 因扩张而重新哈希时,各键值项存储位置都会发生改变。 因此,map
    1: {"user1"}, // 被设计成 not addressable。 类似 m[1].name 这种期望透过原 value
} // 指针修改成员的行为自然会被禁止。

m[1].name = "Tom" // Error: cannot assign to m[1].name

正确做法是完整替换 value 或使用指针。

u := m[1]
u.name = "Tom"
m[1] = u // 替换 value。
m2 := map[int]*user{
    1: &user{"user1"},
}

m2[1].name = "Jack" // 返回的是指针复制品。透过指针修改原对象是允许的。

可以在迭代时安全删除键值。但如果期间有新增操作,那么就不知道会有什么意外了。

for i := 0; i < 5; i++ {
    m := map[int]string{
        0: "a", 1: "a", 2: "a", 3: "a", 4: "a",
        5: "a", 6: "a", 7: "a", 8: "a", 9: "a",
    }

    for k := range m {
        m[k+k] = "x"
        delete(m, k)
    }

    fmt.Println(m)
}

输出:

map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 20:x 28:x 36:x]
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 2:x 6:x 10:x 14:x 18:x]
map[12:x 16:x 20:x 28:x 36:x]

4.4 Struct值类型,赋值和传参会复制全部内容。可用 "_" 定义补位字段,支持指向自身类型的指针成员。

type Node struct {
    _ int
    id int
    data *byte
    next *Node
}

func main() {
    n1 := Node{
        id: 1,
        data: nil,
    }

    n2 := Node{
        id: 2,
        data: nil,
        next: &n1,
    }
}

顺序初始化必须包含全部字段,否则会出错。

type User struct {
    name string
    age int
}

u1 := User{"Tom", 20}
u2 := User{"Tom"} // Error: too few values in struct initializer

支持匿名结构,可用作结构成员或定义变量。

type File struct {
    name string
    size int
    attr struct {
        perm int
        owner int
    }
}

f := File{
    name: "test.txt",
    size: 1025,
    // attr: {0755, 1}, // Error: missing type in composite literal
}

f.attr.owner = 1
f.attr.perm = 0755

var attr = struct {
    perm int
    owner int
}{2, 0755}

f.attr = attr

支持 "=="、"!=" 相等操作符,可用作 map 键类型。

type User struct {
    id int
    name string
}

m := map[User]int{
    User{1, "Tom"}: 100,
}

可定义字段标签,用反射读取。标签是类型的组成部分。

var u1 struct { name string "username" }
var u2 struct { name string }

u2 = u1 // Error: cannot use u1 (type struct { name string "username" }) as
    // type struct { name string } in assignment

空结构 "节省" 内存,比如用来实现 set 数据结构,或者实现没有 "状态" 只有方法的 "静态类"。

var null struct{}

set := make(map[string]struct{})
set["a"] = null

4.4.1 匿名字段

匿名字段不过是一种语法糖,从根本上说,就是一个与成员类型同名 (不含包名) 的字段。被匿名嵌入的可以是任何类型,当然也包括指针。

type User struct {
    name string
}

type Manager struct {
    User
    title string
}
m := Manager{
    User: User{"Tom"}, // 匿名字段的显式字段名,和类型名相同。
    title: "Administrator",
}

可以像普通字段那样访问匿名字段成员,编译器从外向内逐级查找所有层次的匿名字段,直到发现目标或出错。

type Resource struct {
    id int
}

type User struct {
    Resource
    name string
}

type Manager struct {
    User
    title string
}

var m Manager
m.id = 1
m.name = "Jack"
m.title = "Administrator"

外层同名字段会遮蔽嵌入字段成员,相同层次的同名字段也会让编译器无所适从。解决方法是使用显式字段名。

type Resource struct {
    id int
    name string
}

type Classify struct {
    id int
}

type User struct {
    Resource // Resource.id 与 Classify.id 处于同一层次。
    Classify
    name string // 遮蔽 Resource.name。
}

u := User{
    Resource{1, "people"},
    Classify{100},
    "Jack",
}

println(u.name) // User.name: Jack
println(u.Resource.name) // people

// println(u.id) // Error: ambiguous selector u.id
println(u.Classify.id) // 100

不能同时嵌入某一类型和其指针类型,因为它们名字相同。

type Resource struct {
    id int
}

type User struct {
    *Resource
    // Resource // Error: duplicate field Resource
    name string
}

u := User{
    &Resource{1},
    "Administrator",
}

println(u.id)
println(u.Resource.id)

4.4.2 面向对象

面向对象三大特征里,Go 仅支持封装,尽管匿名字段的内存布局和行为类似继承。没有 class 关键字,没有继承、多态等等。

type User struct {
    id int
    name string
}

type Manager struct {
    User
    title string
}

m := Manager{User{1, "Tom"}, "Administrator"}

// var u User = m // Error: cannot use m (type Manager) as type User in assignment
                 // 没有继承,自然也不会有多态。
var u User = m.User // 同类型拷贝。

内存布局和 C struct 相同,没有任何附加的 object 信息。

golang基础语法-1

image

可用 unsafe 包相关函数输出内存地址信息。

m : 0x2102271b0, size: 40, align: 8
m.id : 0x2102271b0, offset: 0
m.name : 0x2102271b8, offset: 8
m.title: 0x2102271c8, offset: 24

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

强化学习精要

强化学习精要

冯超 / 电子工业出版社 / 2018-6 / 80

《强化学习精要:核心算法与TensorFlow 实现》用通俗幽默的语言深入浅出地介绍了强化学习的基本算法与代码实现,为读者构建了一个完整的强化学习知识体系,同时介绍了这些算法的具体实现方式。从基本的马尔可夫决策过程,到各种复杂的强化学习算法,读者都可以从本书中学习到。本书除了介绍这些算法的原理,还深入分析了算法之间的内在联系,可以帮助读者举一反三,掌握算法精髓。书中介绍的代码可以帮助读者快速将算法......一起来看看 《强化学习精要》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换