Golang的复合数据类型|Go主题月

674 阅读11分钟

复合数据类型

数组

  • 数组是具有固定长度且拥有零个或多个相同数据类型元素的序列。但是由于数组的长度固定,所以在Go里很少使用。slice的长度可以增长和缩短,在很多情况下使用的更多

  • 在数组字面量中,如果省略号“...”出现在数组长度的位置,那么数组的长度由初始化数组的元素个数决定。

    q := [...]int{1, 2, 3}
    fmt.Println("%T\n", q)	// "[3]int"
    
  • 数组的长度是数组类型的一部分,这意味着:①:[3]int[4]int不是同一个类型。②:数组的长度必须在编译的时候就能确定下来,是一个常量表达式。

slice

  • slice表示一个拥有相同类型元素的可变长度的序列。slice通常写成[]T,其中元素类型都是T。

  • slice可以用来访问数组的部分或全部元素,而这个数组称为slice的底层数组

  • slice有三个属性:指针、长度和容量。指针指向数组的第一个可以从slice中访问的元素,这个元素不一定的底层数组的第一个元素,是slice的第一个元素;长度是指slice中元素个数,它不能超过slice的容量。容量的大小通常是从slice的起始元素到底层数组的最后一个元素间元素的个数。使用len和cap函数来返回slice的长度和容量。

  • 使用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]
        }
    }
    func main() {
        a := [...]int{0, 1, 2, 3, 4, 5}
        reverse(a[:])
        fmt.Println(a)	// 输出:[5 4 3 2 1 0]
    }
    
  • slice无法进行比较,标准库提供了高度优化的函数bytes.Equal来比较两个字节slice([]byte)。但是对于其他类型的slice,我们必须自己写函数来比较

append函数

  • 内置函数append用来将元素追加到slice的后面

  • 对一个slice进行append的时候,如果该slice的cap不足了,那么会分配一个新的底层数组给它,该底层数组的大小是之前的一倍

  • 在调用append函数的时候,形式如下:

    runes = append(runes, r)
    
  • append函数详解

    • 通常情况下append调用不清楚会不会导致一次新的内存分配(会不会对slice进行扩容),所以我们不能假设原始的slice和使用append后的结果slice指向同一个底层数组,也无法证明它们就指向不同的底层数组。同样,我们也无法假设旧的slice上对元素的操作会或者不会影响新的slice元素。所以,通过我们将append的调用结果再次赋值给传入append函数的slice:

    • 要更新一个slice的指针,无论是否是使用了append函数,长度或容量必须使用如上所示的显示赋值。从这个角度看,slice并不是纯引用类型,而像下面这种聚合类型:

      type slice struct {
          array unsafe.Pointer
          len   int
          cap   int
      }
      

slice就地修改

  • slice可以用来实现栈的数据结构。给定一个空的slice元素stack,可以使用append想slice尾部加值:

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

    栈的顶部是最后一个元素

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

    通过弹出最后一个元素来缩减栈

    stack = stack[:len(stack) - 1]	// pop
    
  • 为了从slice中间删除一个元素,并保留剩余的元素,可以使用函数copy来将高位索引的元素向前移动来覆盖被移动元素所在的位置:

    func remove(slice []int, i int) []int {
        copy(slice[i:], slice[i+1:])
        return slice[:len(slice)-1]
    }
    

map

  • 散列表是一个拥有键值对元素的无序集合。集合中键值唯一,通过键值获取、更新或移除对应的value值。无论表有多大,这些操作基本上都是可以在常量时间的键比较就可以完成

  • 在Go中,map是散列表的引用,map的类型是map[K]V,其中K和V是字典的键和值对应的数据类型,但是键的类型和值的类型不一定相同。

  • 键的类型K,必须是可以通过操作符==来进行比较的数据结构,所以map可以检测某一个键是否已经存在。

  • 虽然浮点数是可以进行比较的,但是比较浮点数的相等性不是一个好主意。

  • 创建map:

    ages := make(map[string]int)
    

    创建一个带初始化键值对的map:

    ages := map[string]int{
        "alice":	31,
        "charlie":	34,
    }
    
  • 删除元素:

    delete(ages, "alice")	// 移除元素 ages["alice"]
    

    这里的alice可以是一个不存在的key,这样也不会报错

  • 遍历map

    for name, age := range ages {
        fmt.Printf("%s\t%d\n", name, age)
    }
    
  • 注意:无法对map中的值进行取内存操作,原因之一是map的增长可能会导致已有元素会被重新散列到新的存储位置

  • map类型的零值是nil,可以对map的零值nil执行查找元素、删除元素,获取map元素个数、执行range循环。但是,对零值map中设置元素会导致错误,例如:

    var ages map[string]int
    ages["carol"] = 21
    

    设置元素前必须要初始化map

  • 通过如下的方式来查询map元素

    age, ok := ages["bob"]
    

    第二个参数ok用来报告该元素是否存在

  • 可以通过map来模拟集合的功能

    func main() {
        seen := make(map[string]bool)	//字符串集合
        input := bufio.NewScanner(os.Stdin)
        for input.Scan() {
            line := input.Text()		// 获取输入的一个字符串
            if !seen[line] {	// 如果该字符串不存在的话,该key对应的value为该value类型的零值也就是false
                seen[line] = true	// 将该字符串加入map中,让该key对应的value为true
                fmt.Println(line)
            }
        }
        if err := input.Err(); err != nil {
            fmt.Printf(os.Stderr, "dedup: %v\n", err)
            os.Exit(1)
        }
    }
    
  • map的键必须是可以比较的,所以无法使用slice作为map的键,当我们有这个需求的时候,可以通过如下的方式实现:首先,定义一个帮助函数k将每一个键都映射到字符串,当且仅当x和y相等的时候,我们才认为k(x) == k(y)。然后,就可以创建一个map,map的键是字符串类型,在每个键元素被访问的时候,调用这个帮助函数。例如:

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

    这个例子中的函数k就是上面说的帮助函数,在向m这个map中添加元素的时候,使用k来将切片转换为字符串

  • map的值类型也可以是复合数据类型,例如:

    map[string]map[string]bool
    

    该map的键类型是string,值类型是map[string]bool

结构体

  • 结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型。每个变量叫做结构体的成员。例如:

    type Employee struct {
        ID int
        Name string
        Address string
        DoB time.Time
        Position string
        Salary int
        ManagerID int
    }
    var dilbert Employee
    
  • 上面例子中的变量dilbert的每一个成员都通过点号的方式来访问,例如dilbert.Name这样

  • 结构体指针也可以使用点号来访问成员变量,例如:

    var employeeOfTheMonth *Employee = &dilbert
    employeeOfTheMonth.Position += " (proactive team player)"
    
  • 成员变量的顺序对于结构体同一性很重要。如果我们将上面结构体Employee,中的三个相同类型的NameAddressPosition组合在一起,或者将NameAddress的顺序进行了互换,那么这就是一个不同的结构体类型

  • 如果一个结构体的成员变量名称是首字母大写的,那么这个变量是可导出的,这是Go最主要的访问控制机制。一个结构体可以同时包含可导出和不可导出的成员变量

  • 命名结构体类型S不可以定义一个拥有相同结构体类型S的成员变量,也就是一个聚合类型不可以包含它自己(同样的限制对数组也使用)。但是S中可以定义一个S的指针类型,即*S,这样我们就可以创建一些递归数据结构,比如链表和树

结构体字面量

  • 结构体类型的值可以通过结构体字面量来设置,即通过设置结构图的成员变量来设置

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

    不建议使用上面这种格式的字面量,因为上面这种格式会增加开发和阅读人员的负担,需要记住每个成员变量的顺序;建议使用如下的方式:

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

    如果这种初始化方式中某个成员变量没有指定,那么它的值就是该成员变量类型的零值。因为指定了成员变量的名字,所以顺序是无所谓的。

  • 可以使用一种更简单的方式来创建、初始化一个结构体变量饼并获取它的地址

    pp := &Point{1, 2}
    // 等价于
    pp := new(Point)
    *pp := Point(1, 2)
    

结构体比较

  • 如果结构体的所有成员变量都可以比较,那么这个结构体就是可比较的
  • 因为结构体是可比较的,所以他可以用来作为map的key

结构体嵌套和匿名成员

  • 可以将一个命名结构当作另一个结构体类型的匿名成员使用

    type Point struct {
        X, Y int
    }
    type Circle struct {
        Center Point
        Radius int
    }
    
  • Go允许我们定义不带名称的结构体成员,只需要指定类型即可;这个结构体成员称做匿名成员

    // 对上面的定义进行简化
    type Point struct {
        X, Y int
    }
    type Circle struct {
        Point
        Radius int
    }
    // 这样我们就能通过 w.X,w.Y 来获取对应的值
    
  • 嵌套的结构体没有什么快捷方式来初始化结构体,必须遵循内部类型的定义

    var c Circle
    c = Circle{8, 8, 6}				// 编译错误,未知成员变量
    c = Circle{X: 8, Y: 8, Radius: 6}	// 编译错误,未知成员变量
    c = Circle{Point{8, 8}, 5}				// 通过
    c = Circle{Point: Point{X: 8, Y: 8}, Radius: 6}	// 通过
    
  • 使用fmt.Printf("%#v\n", c)可以将结构体中的类型也输出来

    fmt.Printf("%#v\n", c)	// main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}
    

JSON

  • Go的标准库encoding包中有很多与JSON\、XML、ANS.1等格式相关的编码和解码API,这里我们介绍encoding\json

  • JSON的基本类型是数组、布尔值、字符串。字符串是用双引号括起来的Unicode代码点的序列,使用反斜杠作为转义字符

  • 这些基础类型通过JSON的数组和对象进行组合

    • JSON的数组是一个有序的元素序列,每个元素之间用逗号分隔,两边用方括号括起来。JSON的数组用来编码Go里边的数组和slice
    • JSON的对象是一个从字符串到值的映射,写成name:value对的序列,每个元素之间使用逗号分隔,两边用花括号括起来。JSON的对象用来编码Go里边的map(键为字符串类型)和结构体
  • 把Go的数据结构转化为JSON称为marshal,通过encoding/json包中的json.Marshal来实现

    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)  // 该方法的输出是不格式化的,全部挤在一起
        data, err := json.MarshalIndent(movies, "", "	")	// 该方法的输出是格式化的
        if err != nil {
            log.Fatalf("JSON marshaling failed: %s", err)
        }
        fmt.Printf("%s\n", data)
    }
    
    • marshal使用Go结构体成员的名称作为json对象的里面字段的名称(通过反射)。只有可导出的的成员可以转化为JSON字段,这就是为什么我们将Go结构体中的所有成员都定义为首字母大写
  • 上面代码的输出

    ```json
    [
        {
            "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"
            ]
        }
    ]
    
      
      可以看到结构体中的`Year`对应转换为了`released`,另外`Color`转换为了`color`。这个是通过`成员标签定义(field tag)`实现的。`Color`的标签还有一个额外的选项`omitempty`,它表示如果这个成员的值是零值或者为空,则不输出这个成员到JSON中。
    
  • marshal的逆操作将JSON字符串解码为Go数据结构,这个过程叫做unmarshal,这个是由json.unmarshal实现的。

    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}]"
    
  • 另外还有两个方法json.Decoderjson.Encoder可以用来从字节流中解码出多个JSON实体和将JSON实体编码成字节流的形式输出,并会带上一个换行符。