zoukankan      html  css  js  c++  java
  • 04复合数据类型

    一:数组

             1:数组的长度是固定的,因此在Go中很少直接使用数组。和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列。

     

             2:len函数返回数组中元素的个数。

    var a [3]int                // array of 3 integers
    fmt.Println(a[0])           // print the first element
    fmt.Println(a[len(a)-1])    // print the last element, a[2]
    // Print the indices and elements.
    for i, v := range a {
        fmt.Printf("%d %d
    ", i, v)
    }
    // Print the elements only.
    for _, v := range a {
        fmt.Printf("%d
    ", v)
    }

    3:默认情况下,数组的每个元素都被初始化为元素类型对应的零值

    var r [3]int = [3]int{1, 2}
    fmt.Println(r[1]) // "2"
    fmt.Println(r[2]) // "0"

    4:在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算:

    q := [...]int{1, 2, 3}
    fmt.Printf("%T
    ", q) // "[3]int"

    5:数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定

     

    6:可以指定一个索引和对应值列表的方式初始化,就像下面这样:

    type Currency int
    const (
        USD Currency = iota // 美元
        EUR // 欧元
        GBP // 英镑
        RMB // 人民币
    )
    symbol := [...]string{USD: "$", EUR: "", GBP: "", RMB: ""}
    fmt.Println(RMB, symbol[RMB]) // "3 ¥"

    在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。

    比如r := [...]int{99: -1}定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。

     

    7:如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组

     

    8:其它很多编程语言不同,当数组作为函数参数时,Go语言不会隐式地将数组作为引用或指针对象传入被调用的函数。在Go中,当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。

    可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者:

    func change1(r [3]int) {
        r[2] = 100
    }
    func change2(r *[3]int) {
        r[2] = 100
    }
    
    func main() {
        var r [3]int = [3]int{1,2,3}
        fmt.Println(r)  //[1 2 3]
        
        change1(r)
        fmt.Println(r)  //[1 2 3]
        
        change2(&r)
        fmt.Println(r)  //[1 2 100]
    }

    9:数组是僵化的类型,因为数组的类型包含了僵化的长度信息。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。

     

    二:Slice

    1:Slice(切片)代表变长的序列。slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

    数组和slice之间有着紧密的联系,slice的底层引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

     

    2:切片操作s[i:j],其中0≤i≤j≤cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。如果省略i的话将使用0代替,如果省略j的话将使用len(s)代替:

    months := [...]string{1: "January", 2: "February", 3: "March", 4: "April",
                        5: "May", 6: "jjune", 7: "July", 8: "August", 9: "September",
                        10: "October", 11: "November", 12: "December"}
                        
    Q2 := months[4:7]
    
    fmt.Printf("Type of months is %T, len is %d, cap is %d
    ", months, len(months), cap(months))
    fmt.Printf("Type of Q2 is %T, len is %d, cap is %d
    ", Q2, len(Q2), cap(Q2))

     结果是

    Type of months is [13]string, len is 13, cap is 13
    Type of Q2 is []string, len is 3, cap is 9

    3:多个slice之间可以共享底层的数据,并且引用的数组部分区间可以重叠。修改底层数组元素的值将会影响切片元素:

    Q2 := months[4:7]
    summer := months[6:9]
    fmt.Println(Q2)     // ["April" "May" "jjune"]
    fmt.Println(summer) // ["jjune" "July" "August"]    
    
    months[6] = "June"
    fmt.Println(Q2)     // ["April" "May" "June"]
    fmt.Println(summer) // ["June" "July" "August"] 

    4:如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:

    fmt.Println(summer[:20])    // panic: out of range
    endlessSummer := summer[:5] // extend a slice (within capacity)
    
    fmt.Println(summer)         // "[June July August]"
    fmt.Println(endlessSummer)  // "[June July August September October]"

    5:字符串也支持切片操作,但是字符串的切片操作x[m:n]生成的是一个新字符串:

    str := "hello, world"
    s := str[:5]
    
    fmt.Printf("Type of str is %T
    ", str)  //Type of str is string
    fmt.Printf("Type of s is %T
    ", s)      //Type of s is string

    6:向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。下面的reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice或数组:

    func reverse(s []int) {
        for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
            s[i], s[j] = s[j], s[i]
        }
    }
    
    a := [...]int{0, 1, 2, 3, 4, 5}
    s := a[:]
    fmt.Println(s)  //[0 1 2 3 4 5]
    
    reverse(s)
    fmt.Println(s) // [5 4 3 2 1 0]
    
    reverse(s)
    fmt.Println(s) // [0 1 2 3 4 5]

    7:就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定,或者的两种风格的混合语法初始化。

    a := []int{0, 1, 2, 3, 4, 5}
    b := []int{5:5, 7}
    
    fmt.Printf("%T, %T
    ", a, b) //[]int, []int
    fmt.Println(a)  //[0 1 2 3 4 5]
    fmt.Println(b) // [0 0 0 0 0 5]

    8:和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较。

    slice唯一合法的比较操作是和nil比较,例如:

    if summer == nil { /* ... */ }

    一个nil值的slice并没有底层数组,其长度和容量都是0。

    也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。

    可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值:

    var s []int // len(s) == 0, s == nil
    s = nil // len(s) == 0, s == nil
    s = []int(nil) // len(s) == 0, s == nil
    s = []int{} // len(s) == 0, s != nil

    9:内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

    make([]T, len)
    make([]T, len, cap) // same as make([]T, cap)[:len]

    在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。

     

    10:内置的append函数用于向slice追加元素:

    var runes []rune
    fmt.Printf("len is %d, cap is %d, runes is %q
    ", len(runes), cap(runes), runes)
    
    for _, r := range "Hello, 世界" {
        runes = append(runes, r)
        fmt.Printf("len is %d, cap is %d, runes is %q
    ", len(runes), cap(runes), runes)
    }

    结果是:

    len is 0, cap is 0, runes is []
    len is 1, cap is 2, runes is ['H']
    len is 2, cap is 2, runes is ['H' 'e']
    len is 3, cap is 4, runes is ['H' 'e' 'l']
    len is 4, cap is 4, runes is ['H' 'e' 'l' 'l']
    len is 5, cap is 8, runes is ['H' 'e' 'l' 'l' 'o']
    len is 6, cap is 8, runes is ['H' 'e' 'l' 'l' 'o' ',']
    len is 7, cap is 8, runes is ['H' 'e' 'l' 'l' 'o' ',' ' ']
    len is 8, cap is 8, runes is ['H' 'e' 'l' 'l' 'o' ',' ' ' '']
    len is 9, cap is 16, runes is ['H' 'e' 'l' 'l' 'o' ',' ' ' '' '']

    每次调用append函数,会先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的元素复制到新扩展的空间,并返回slice。因此,输入的slice和输出的slice共享相同的底层数组。如果没有足够的增长空间的话,append函数则会先分配一个足够大的slice用于保存新的结果,先将输入的slice复制到新的空间,然后添加元素。结果slice和输入的slice引用的将是不同的底层数组。append函数并不受cap的影响。

    因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量: runes = append(runes, r)

    更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:

    type IntSlice struct {
        ptr *int
        len, cap int
    }

    append函数则可以追加多个元素,甚至追加一个slice:

    var x []int
    x = append(x, 1)
    x = append(x, 2, 3)
    x = append(x, 4, 5, 6)
    x = append(x, x...) // append the slice x
    fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"

     

    11:slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用append函数将新的值压入stack:

    stack = append(stack, v) // push v

    stack的顶部位置对应slice的最后一个元素:

    top := stack[len(stack)-1] // top of stack

    通过收缩stack可以弹出栈顶的元素

    stack = stack[:len(stack)-1] // pop

    12:要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成:

    func remove(slice []int, i int) []int {
        copy(slice[i:], slice[i+1:])
        return slice[:len(slice)-1]
    }
    func main() {
        s := []int{5, 6, 7, 8, 9}
        fmt.Println(remove(s, 2)) // "[5 6 8 9]"
        fmt.Println(s) //[5 6 8 9 9]
    }

    内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数,等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围:

    s := []int{5, 6, 7, 8, 9}
    d := copy(s[4:], s[1:])
    fmt.Println(s)  //[5 6 7 8 6]
    fmt.Println(d)  //1

     

    三:Map

    1:哈希表是一个无序的key/value对的集合,其中所有的key都是不同的,通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

    在Go中,一个map就是一个哈希表的引用。map类型可以写为map[K]V,map中所有的key都有相同的类型,所有的value也有着相同的类型,其中K对应的key必须是支持==比较运算符的数据类型。注意,虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法。对于V对应的value数据类型则没有任何的限制。

     

    2:内置的make函数可以创建一个map:

    ages := make(map[string]int)

    也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:

    ages := map[string]int{
        "alice": 31,
        "charlie": 34,
    }

    要注意34后面的逗号不能省略,否则会引起编译错误(这是因为编译器会自动在某些字符后加分号造成的)。创建空的map的表达式是 map[string]int{} 。

     

    4:Map中的元素通过key对应的下标语法访问:

    ages["alice"] = 32
    fmt.Println(ages["alice"]) // "32"

    使用内置的delete函数可以删除元素:

        delete(ages, "alice")
        fmt.Println(ages["alice"]) // "0"

    所有这些操作是安全的,即使这些元素不在map中也没有关系。key查找失败时将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。

    ages["bob"] = ages["bob"] + 1 // happy birthday!

    而且 x += y 和 x++ 等简短赋值语法也可以用在map上,所以上面的代码可以改写成

    ages["bob"] += 1
    ages["bob"]++

    5:map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

    _ = &ages["bob"] // compile error: cannot take address of map element

    禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

     

    6:可以使用range风格的for循环来遍历map中全部的key/value对:

    for name, age := range ages {
        fmt.Printf("%s	%d
    ", name, age)
    }

    map的迭代顺序是不确定的,在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。

    如果要按顺序遍历key/value对,必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序:

        names := make([]string, 0, len(ages))
        for name := range ages {
            names = append(names, name)
        }
        sort.Strings(names)
        for _, name := range names {
            fmt.Printf("%s	%d
    ", name, ages[name])
        }

    一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。

     

    7:map类型的零值是nil,也就是没有引用任何哈希表。

        var ages map[string]int
        fmt.Println(ages == nil) // "true"
        fmt.Println(len(ages) == 0) // "true"
        
        var ages = map[string]int{}
        fmt.Println(ages == nil) // "false"

    map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:

        ages["carol"] = 21 // panic: assignment to entry in nil map

    8:通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值。

    这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可以需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:

        age, ok := ages["bob"]
        if !ok { /* "bob" is not a key in this map; age == 0. */ }

    更加简洁的写法是:

    if age, ok := ages["bob"]; !ok { /* ... */ }

    在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

     

    9:map是对哈希表的引用,因此向函数传递map将允许在函数内部修改其元素:

    func change(m map[string]int) {
        m["100"] = 100
    }
    
    ages := map[string]int{
        "1":1,
        "2":2,
    }
    fmt.Println(ages)   //map[1:1 2:2]
    change(ages)
    fmt.Println(ages)   //map[1:1 2:2 100:100]

    10:和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现。

     

    11:Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。

     

             12:有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型:

    var m = make(map[string]int)
    func k(list []string) string { return fmt.Sprintf("%q", list) }
    func Add(list []string) { m[k(list)]++ }
    func Count(list []string) int { return m[k(list)] }

    使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。

     

     

    四:结构体

    1:下面两个语句声明了一个叫Employee的结构体类型,并且声明了一个Employee类型的变量dilbert:

    type Employee struct {
        ID int
        Name string
        Address string
        DoB time.Time
        Position string
        Salary int
        ManagerID int
    }
    var dilbert Employee

    dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Name和dilbert.DoB。可以直接对每个成员赋值或者是对成员取地址,然后通过指针访问:

        dilbert.Salary -= 5000 // demoted, for writing too few lines of code
        position := &dilbert.Position
        *position = "Senior " + *position // promoted, for outsourcing to Elbonia

    点操作符也可以和指向结构体的指针一起工作:

        var employeeOfTheMonth *Employee = &dilbert
        employeeOfTheMonth.Position += " (proactive team player)"

    相当于下面的语句

        (*employeeOfTheMonth).Position += " (proactive team player)"

    2:通常一行对应一个结构体成员,成员的名字在前类型在后。如果相邻的成员类型相同的话可以被合并到一行,就像下面的Name和Address成员那样:

    type Employee struct {
        ID int
        Name, Address string
        DoB time.Time
        Position string
        Salary int
        ManagerID int
    }

    结构体成员的输入顺序也有重要的意义。如果交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。

     

    3:如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;

     

    4:一个名为S的结构体类型将不能再包含S类型的成员,因为一个聚合的值不能包含它自身(该限制同样适应于数k组)。但是S类型的结构体可以包含 *S 指针类型的成员。下面的代码中,我们使用一个二叉树来实现一个插入排序:

    type tree struct {
        value int
        left, right *tree
    }
    
    func add(t *tree, value int) *tree {
        if t == nil {
            // Equivalent to return &tree{value: value}.
            t = new(tree)
            t.value = value
            return t
        }
        if value < t.value {
            t.left = add(t.left, value)
        } else {
            t.right = add(t.right, value)
        }
        return t
    }
    
    func appendValues(values []int, t *tree) []int {
        if t != nil {
            values = appendValues(values, t.left)
            values = append(values, t.value)
            values = appendValues(values, t.right)
        }
        return values
    }
    // Sort sorts values in place.
    func Sort(values []int) {
        var root *tree
        for _, v := range values {
            root = add(root, v)
        }
        appendValues(values[:0], root)
    }

             5:结构体类型的零值是每个成员都是零值。

    如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息。

     

    6:结构体值也可以用结构体面值表示,结构体面值可以指定每个成员的值:

    type Point struct{ X, Y int }
    p := Point{1, 2}

    这种写法要求以结构体成员定义的顺序为每个结构体成员指定一个面值。缺点是结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用。

     

    更常用的写法是,以成员名字和相应的值来初始化,可以包含部分或全部的成员:

    p := Point{Y:1, X:2}

    在这种形式的结构体面值写法中,如果成员被忽略的话将默认用零值。因为,提供了成员的名字,所有成员出现的顺序并不重要。

     

    两种不同形式的写法不能混合使用。而且不能在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。

    package p
    type T struct{ a, b int } // a and b are not exported
    //////////////////////////////////
    package q
    import "p"
    var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
    var _ = p.T{1, 2} // compile error: can't reference a, b

    7:结构体可以作为函数的参数和返回值。如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,如果要在函数内部修改结构体成员的话,用指针传入是必须的。

     

    8:如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。

     

    9:结构体嵌入和匿名成员

    考虑下面表示图形的结构体:

    type Circle struct {
        X, Y, Radius int
    }
    type Wheel struct {
        X, Y, Radius, Spokes int
    }

    一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。

    为了便于维护而将相同的属性独立出来:

    type Point struct {
        X, Y int
    }
    type Circle struct {
        Center Point
        Radius int
    }
    type Wheel struct {
        Circle Circle
        Spokes int
    }

    这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:

    var w Wheel
    w.Circle.Center.X = 8
    w.Circle.Center.Y = 8
    w.Circle.Radius = 5
    w.Spokes = 20

    在Go中,可以只声明一个成员对应的数据类型而不指定成员的名字,这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

    type Circle struct {
        Point
        Radius int
    }
    type Wheel struct {
        Circle
        Spokes int
    }

    得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

    var w Wheel
    w.X = 8 // equivalent to w.Circle.Point.X = 8
    w.Y = 8 // equivalent to w.Circle.Point.Y = 8
    w.Radius = 5 // equivalent to w.Circle.Radius = 5
    w.Spokes = 20

    在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。

    不幸的是,结构体字面值并没有简短表示匿名成员的语法,因此下面的语句都不能编译通过:

    w = Wheel{8, 8, 5, 20} // compile error: unknown fields
    w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

    结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

    w = Wheel{Circle{Point{8, 8}, 5}, 20}
    w = Wheel{
        Circle: Circle{
            Point: Point{X: 8, Y: 8},
            Radius: 5,
        },
        Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
    }
    fmt.Printf("%#v
    ", w) // Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
    w.X = 42
    fmt.Printf("%#v
    ", w) // Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}

    需要注意的是结构体最后一个成员后的逗号不能省略,否则会报编译错误(这是因为编译器会自动在某些字符后加分号造成的);Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。

     

    因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员w.X = 8 // equivalent to w.circle.point.X = 8。但是在包外部,因为circle和point没有导出不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。

     

     

    五:JSON

             基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列。这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;JSON数组可以用于编码Go中的数组和slice。一个JSON对象是一个字符串到值的映射,写成name:value的形式,用花括号包含并以逗号分隔;JSON的对象类型可用于编码Go中的map类型(key类型是字符串)和结构体。

     

    2:将一个Go中的数据结构转为JSON的过程叫编组(marshaling)。编组可以通过调用json.Marshal函数完成:

    import "encoding/json"
    import "fmt"
    
    type Movie struct {
        Title string
        Year int `json:"released"`
        Color bool `json:"color,omitempty"`
        Actors []string
    }
    var movies = []Movie{
        {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    
        {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    
        {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    }
    
    func main() {
        data, err := json.Marshal(movies)
        if err != nil {
            fmt.Printf("JSON marshaling failed: %s", err)
        }
        fmt.Printf("%s
    ", data)
    }

    结果是:

    [{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingrid Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Actors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"Actors":["Steve McQueen","Jacqueline Bisset"]}]

    这种紧凑的表示形式很难阅读,可以使用json.MarshalIndent函数产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

    data, err := json.MarshalIndent(movies, "", "   ")

    结果如下:

    [
       {
          "Title": "Casablanca",
          "released": 1942,
          "Actors": [
             "Humphrey Bogart",
             "Ingrid Bergman"
          ]
       },
       {
          "Title": "Cool Hand Luke",
          "released": 1967,
          "color": true,
          "Actors": [
             "Paul Newman"
          ]
       },
       {
          "Title": "Bullitt",
          "released": 1968,
          "color": true,
          "Actors": [
             "Steve McQueen",
             "Jacqueline Bisset"
          ]
       }
    ]

    3:只有导出的结构体成员才会被编码。在编码时,默认使用Go语言结构体的成员名字作为JSON的对象名。

    细心的读者可能已经注意到,其中Year名字的成员在编码后变成了released,还有Color成员编码后变成了小写字母开头的color。这是因为构体成员Tag所导致的。一个构体成员Tag是在编译阶段关联到该成员的元信息字符串:

        Year int `json:"released"`
        Color bool `json:"color,omitempty"`

    结构体的成员Tag通常是一系列用空格分隔的key:"value"键值对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。以json开头的键名用于控制encoding/json包的编码和解码的行为。成员Tag中json对应值的第一部分用于指定JSON对象的名字,比如将Year成员对应到JSON中的released对象;另外Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成JSON对象(Casablanca是一个黑白电影,并没有输出Color成员)

     

    4:编码的逆操作是解码(unmarshaling),也就是将JSON数据解码为Go语言的数据结构。解码通过json.Unmarshal函数完成。通过定义合适的Go语言数据结构,我们可以选择性地解码JSON中感兴趣的成员:

    var titles []struct{ Title string }
    if err := json.Unmarshal(data, &titles); err != nil {
        log.Fatalf("JSON unmarshaling failed: %s", err)
    }
    fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"

    六:文本和HTML模板

    使用Printf可以进行最简单的格式化。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。可以使用text/template和html/template等模板包提供的功能,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。

    一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的 {{action}} 对象。大部分的字符串只是按面值打印,但是对于{{action}}部分将触发其它的行为。每个{{action}}都包含了一个用模板语言书写的表达式,表达式包括选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串:

    type IssuesSearchResult struct {
        TotalCount int `json:"total_count"`
        Items []*Issue
    }
    type Issue struct {
        Number int
        HTMLURL string `json:"html_url"`
        Title string
        State string
        User *User
        CreatedAt time.Time `json:"created_at"`
        Body string // in Markdown format
    }
    type User struct {
        Login string
        HTMLURL string `json:"html_url"`
    }
    
    const templ = `{{.TotalCount}} issues:
    {{range .Items}}----------------------------------------
    Number: {{.Number}}
    User: {{.User.Login}}
    Title: {{.Title | printf "%.64s"}}
    Age: {{.CreatedAt | daysAgo}} days
    {{end}}`

    这个模板是针对IssuesSearchResult结构体定义的,先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。

    对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板时的参数,也就是IssuesSearchResult类型的变量。模板中 {{.TotalCount}} 对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中 {{range .Items}} 和 {{end}} 对应一个循环action,因此它们直接的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。

    在一个action中, | 操作符表示将前一个表达式的结果作为后一个函数的输入,类似于管道的概念。在Title这一行的action中,第二个操作是一个printf函数;对于Age部分,第二个动作是一个叫daysAgo的函数,通过time.Since函数将CreatedAt成员转换为过去的时间长度:

    func daysAgo(t time.Time) int {
        return int(time.Since(t).Hours() / 24)
    }

    生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序:template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。

    report, err := template.New("report").
                    Funcs(template.FuncMap{"daysAgo": daysAgo}).
                    Parse(templ)
    if err != nil {
        log.Fatal(err)
    }

    因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板,

    一旦模板已经创建、注册了daysAgo函数、并通过分析和检测,我们就可以使用IssuesSearchResult作为输入源,以os.Stdout作为输出源来执行模板。下面的代码使用github.SearchIssues函数,该函数向https://api.github.com/search/issues发送HTTP请求,返回一个json结果,并将该json结果转化为IssuesSearchResult变量:

    var report = template.Must(template.New("issuelist").
                        Funcs(template.FuncMap{"daysAgo": daysAgo}).
                        Parse(templ))
    func main() {
        result, err := github.SearchIssues(os.Args[1:])
        if err != nil {
            log.Fatal(err)
        }
        if err := report.Execute(os.Stdout, result); err != nil {
            log.Fatal(err)
        }
    }

    程序输出一个纯文本报告:

    $ ./issuesreport repo:golang/go is:open json decoder
    13 issues:
    ----------------------------------------
    Number: 5680
    User: eaigner
    Title: encoding/json: set key converter on en/decoder
    Age: 750 days
    ----------------------------------------
    Number: 6050
    User: gopherbot
    Title: encoding/json: provide tokenizer
    Age: 695 days
    ----------------------------------------
    ...

    html/template模板包和text/template包具有相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。如果想了解更多的信息,请自己查看包文档:

    $ go doc text/template

    $ go doc html/template

  • 相关阅读:
    Centos安装mysql
    @autowired注解报错原因及解决办法
    注解@Resource与@Autowired的区别
    SpringBoot三种获取Request和Response的方法
    oss 上传图片、下载 中文名称
    git tags 和 Branches的区别是什么呀,什么时候应该创建一个Tag?
    git使用命令,git checkout -b a 与 git branch a区别
    PostgreSQL-With子句实现递归
    redisson锁 tryLock的使用及正确用法
    mysql any 和in 函数的使用
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7871211.html
Copyright © 2011-2022 走看看