Go:复合数据结构

数组

  • 定义:数组是固定长度、元素数据类型相同的序列 。元素通过索引访问,索引从 0 到数组长度减 1 。可用len函数获取元素个数 。

  • 初始化:默认元素初始值为类型零值(数字为 0 ) 。可使用数组字面量初始化,如var q [3]int = [3]int{1, 2, 3} ,也可简化为q := [...]int{1, 2, 3} ,省略号使数组长度由初始化元素个数决定 。

  • 类型特性:数组长度是类型一部分,[3]int[4]int是不同类型 ,且长度须是编译时可确定的常量表达式 。

  • 比较规则:若元素类型可比较,数组也可比较,用==判断两边元素值是否完全相同,!=判断是否不同 。不同长度数组不能比较 。

在函数中的传递

  • 函数传参时,传入数组会创建副本,值传递方式使传递大数组低效,且函数内修改不影响原始数组 。可传递数组指针,函数内对指针指向数组的修改会反映到原始数组 ,如zero函数通过指针将[32]byte数组元素清零 。

slice

  • 定义:slice 是拥有相同类型元素的可变长度序列,写法为[]TT为元素类型,可看作底层数组的动态视图 。

  • 底层结构:底层是数组,slice 有指针(指向数组第一个可访问元素 )、长度(可访问元素个数 )和容量(从第一个元素到底层数组最后一个元素间元素个数 )三个属性 ,用lencap函数获取长度和容量 。

  • 与数组关系:一个底层数组可对应多个 slice ,slice 可引用数组任意位置,元素可重叠 。

  • 创建新 slice:用s[i:j]0 ≤ i ≤ j ≤ cap(s) )操作符创建新 slice ,引用原 slice 从ij - 1索引位置元素 ,省略i默认从 0 开始,省略j默认到len(s) - 1

  • 引用越界情况:引用超过容量会导致程序宕机;超过长度会使最终 slice 比原 slice 长 。

与字符串、数组操作对比

  • 字符串子串操作和字节 slice 操作类似,都写作x[m:n] ,但字符串返回字符串,字节 slice 返回字节 slice 。

在函数中的传递

  • 传递 slice 给函数时,因包含指向数组元素指针,函数内可修改底层数组元素 ,如reverse函数可就地反转整型 slice 元素。

  • 比较:slice 不能直接用==比较是否相同,因元素非直接关联 ,可自定义函数深度比较 。标准库bytes.Equal可比较字节 slice 。

  • 特殊值:值为nil的 slice 长度和容量为零,无对应底层数组 ;非nil但长度和容量为零的 slice 有底层数组 。检查 slice 是否为空用len(s) == 0而非s == nil

创建

  • make函数可创建指定元素类型、长度和容量的 slice ,容量参数可省略,此时长度和容量相等

append 函数

  • 基本用法:内置函数append用于将元素追加到 slice 后面 。

  • 原理append操作需检查 slice 容量。若容量足够,在原底层数组基础上扩展 slice 并添加元素;若容量不足,创建新的足够大容量的底层数组,将原 slice 元素复制到新数组,再追加新元素 。

func main() {
	var x, y []int
	for i := 0; i < 10; i++ {
		y = appendInt(x, i)
		fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
		x = y
	}
}

func appendInt(x []int, y int) []int {
	var z []int
	zlen := len(x) + 1
	if zlen <= cap(x) {
		z = x[:zlen]
	} else {
		zcap := max(zlen, 2 * len(x))
		z = make([]int, zlen, zcap)
		copy(z, x)
	}
	z[len(x)] = y
	return z
}
  • 示例appendInt函数模拟append功能 。当 slice 容量足够(zlen <= cap(x) )时,直接在原 slice 扩展;容量不足时,新数组容量一般扩展为原 slice 长度的 2 倍(通过zcap计算 ),用make创建新数组,再用copy函数将原 slice 元素复制到新数组,最后追加新元素并返回新 slice 。

copy 函数

  • copy函数用于将一个 slice 元素复制到另一个 slice ,参数分别为目标 slice 和源 slice ,返回值是实际复制的元素个数(两个 slice 长度的较小值 ) 。

append 函数增长策略

  • 每次append操作若导致 slice 容量改变,意味着底层数组重新分配和元素复制 。以示例程序展示每次追加元素时 slice 容量变化,如容量不够时会成倍扩展 ,以减少内存分配次数 。实际内置append函数增长策略更复杂,无法预先确定一次调用是否引发新内存分配 。

注意事项

  • 使用append后通常需将结果重新赋值给原 slice 变量 ,因为函数可能改变 slice 指针、长度或容量 。
  • 对于任何可能改变 slice 相关属性的函数调用,都要注意更新 slice 变量 。并且appendInt只能添加单个元素,而内置append可同时添加多个元素甚至另一个 slice 的所有元素 。

slice 就地修改

func nonempty(strings []string) []string {
    i := 0
    for _, s := range strings {
        if s != "" {
            strings[i] = s
            i++
        }
    }
    return stirngs[:i]
}
  • nonempty函数:用于从字符串列表 slice 中去除空字符串 。通过遍历输入的strings slice ,将非空字符串依次复制到原 slice 前面位置,最后返回有效元素部分(strings[:i] ) 。该函数输入和输出的 slice 共用底层数组,避免重新分配数组,也可利用append函数实现类似功能 。

实现栈

  • 可用 slice 实现栈,通过append向 slice 尾部追加元素实现入栈操作(stack = append(stack, v) ) ,栈顶元素为stack[len(stack) - 1] ,通过stack = stack[:len(stack) - 1]实现出栈操作 。

移除操作

  • 保持顺序移除:要从 slice 中间移除元素并保留剩余元素顺序,使用copy函数,将指定位置(i )之后元素向前移动覆盖被移除元素位置,如copy(slice[i:], slice[i + 1:]) ,然后返回处理后的 slice(slice[:len(slice) - 1] ) 。
  • 不保持顺序移除:若无需保持顺序,可将 slice 最后一个元素赋值给被移除元素位置,如slice[i] = slice[len(slice) - 1] ,再返回处理后的 slice(slice[:len(slice) - 1] ) 。

map

  • 定义map是散列表的引用,是拥有键值对元素的无序集合 ,类型为map[K]VK为键类型,V为值类型 。键类型须可通过==操作符比较,值类型无限制 。

创建与初始化

  • make函数创建,如ages := make(map[string]int)
  • 也可用字面量新建带初始化键值对的map ,如ages := map[string]int{"alice": 31, "charlie": 34} ,还可分步赋值 。
  • map表示为map[string]int{}

操作

  • 元素访问:通过下标方式访问,如ages["alice"] ,键不存在时返回值类型零值 。
  • 元素删除:使用delete函数根据键移除元素,如delete(ages, "alice") ,操作安全,键不存在也不会出错 。
  • 元素修改与新增:可通过赋值语句修改或新增元素,如ages["bob"] = ages["bob"] + 1 ,快捷赋值方式(+=++ )同样适用 ,但不能获取map元素地址 。

遍历

  • for循环结合range关键字遍历,如for name, age := range ages { fmt.Printf("%s\t%d\n", name, age) } ,元素迭代顺序不固定 。若要按键顺序遍历,对于字符串键,可借助sort包的Strings函数对键排序后再遍历 。

零值与特殊情况

  • map类型零值是nil ,大部分map操作可在零值map上执行,但向零值map设置元素会导致错误 ,设置元素前需初始化 。
  • 通过下标访问map元素时,可使用多值返回判断元素是否存在 ,如age, ok := ages["bob"]; if!ok { /*...*/ }

比较

  • map不可直接比较,唯一合法比较是和nil比较 。要判断两个map是否相同,需编写循环逐个比较键值对 。
func main() {
	seen := make(map[string]bool)
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() {
		line := input.Text()
		if !seen[line] {
			seen[line] = true
			fmt.Println(line)
		}
	}
	if err := input.Err(); err != nil {
		fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
		os.Exit(1)
	}
}
  • 去重示例dedup程序利用map存储已出现的行,确保相同行不重复输出 。

结构体

type Employee struct {
    ID int
    Name string
    Address string
    DoB time.Time
    Position string
    Salary int
    ManagerId int
}
  • 定义:结构体是将多个任意类型的命名变量组合在一起的聚合数据类型 ,每个变量是结构体成员 。以员工信息记录为例,可定义Employee结构体包含IDNameAddress等成员 。
  • 成员访问与操作
    • 通过点号(. )访问结构体成员,如dilbert.Name
    • 可对成员赋值,如dilbert.Salary -= 5000 ;也可获取成员地址,通过指针访问修改,如position := &dilbert.Position; *position = "Senior " + *position
    • 结构体指针同样用点号访问成员,如employeeOfTheMonth.Position += " (proactive team player)"

定义规则

  • 成员变量一般一行写一个,相同类型的连续成员变量可写在一行 。成员变量顺序影响结构体同一性,改变顺序会定义出不同结构体类型 。
  • 首字母大写的成员变量是可导出的,是 Go 语言的访问控制机制 。

限制与应用

type tree struct {
	value int
	left, right *tree
}

func Sort(values []int) {
	var root *tree
	for _, v := range values {
		root = add(root, v)
	}
	appendValues(values[:0], root)
}

func add(t *tree,  value int) *tree {
	if t == nil {
		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函数构建二叉树并排序、appendValues函数按序追加元素、add函数向二叉树插入节点 。

零值

  • 结构体零值由成员零值组成,很多情况下零值是合理可用的初始状态 ,如bytes.Buffersync.Mutex 。还提到空结构体struct{} ,无成员变量,长度为 0 ,可用于一些节省内存且语法简单的场景 ,如在map中作值类型来替代集合 。

结构体字面量

  • 第一种格式:按成员变量顺序为每个成员指定值 ,如Point结构体,p := Point{1, 2} 。此格式要求记住成员顺序,在结构体成员扩充或顺序调整时不利于代码维护,常用于结构体定义所在包或成员顺序有明显约定的小结构体 ,如image.Pointcolor.RGBA

  • 第二种格式:通过指定部分或全部成员变量的名称和值来初始化结构体变量 ,如anim := gif.GIF{LoopCount: nframes} 。未指定值的成员取其类型零值 ,成员顺序无要求 ,使用更广泛 。

  • 两种初始化方式不能混合使用 ,且第一种方式不能绕过不可导出变量的访问限制 。

  • 值传递:结构体类型的值可作为参数传递给函数或作为函数返回值 ,如Scale函数将Point结构体按比率缩放并返回新的Point

  • 指针传递:出于效率和修改结构体内容的需求,大型结构体常以指针形式传递给函数或从函数返回

创建与初始化

  • 可使用&Point{1, 2}直接创建并初始化Point结构体变量并获取其地址 ,这等价于pp := new(Point); *pp = Point{1, 2} ,且&Point{1, 2}可直接用于表达式或函数调用中 。

结构体比较

  • 若结构体的所有成员变量都可比较,那么这个结构体就是可比较的 。可使用==!=操作符对结构体进行比较 。==操作符按顺序比较两个结构体变量的成员变量 ,以Point结构体为例,p := Point{1, 2}q := Point{2, 1}p == qp.X == q.X && p.Y == q.Y等价,结果都为false
type address struct {
    hostname string
    port int
}
hits := make(map[address]int)
hits[address{"golang.org", 443}]++
  • 和其他可比较类型一样,可比较的结构体类型能作为map的键类型 。

结构体嵌套和匿名成员

type Circle struct {
    X, Y, Radius int
}

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

var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
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

// 等价于上面
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20

以 2D 绘图程序中形状库为例,最初定义CircleWheel结构体 ,Wheel包含Circle所有属性及额外的Spokes属性 。随着形状增多,重构提取出Point结构体,将Circle定义为包含Center PointRadiusWheel包含CircleSpokes ,但此时访问Wheel成员变得繁琐 。

Go 语言允许定义不带名称的结构体成员(匿名成员 ),其类型须是命名类型或指向命名类型的指针 。如Circle结构体中嵌入PointWheel结构体中嵌入Circle 。使用匿名成员后,可直接通过外层结构体访问内层成员 ,如w.X等价于w.Circle.Point.X

结构体字面量初始化

w = Wheel{Circle{point{8, 8}, 5}, 20}
// or
w = Wheel{
    Circle: Circle{
        Point: Point(X: 8, Y: 8),
        Radius = 5,
    }
    Spokes: 20, // 尾部的逗号是必需的
}
  • 结构体字面量不能直接以简洁方式初始化嵌套结构体 ,需遵循形状类型定义 。给出两种合法初始化Wheel结构体的方式 ,一种是w = Wheel{Circle{Point{8, 8}, 5}, 20} ,另一种是详细展开的方式 。

注意事项

  • 一个结构体中不能定义两个相同类型的匿名成员,会引起冲突 。
  • 匿名成员的可导出性由其类型决定 ,即使外层结构体不可导出,仍可通过快捷方式访问可导出匿名成员的内部变量 ,但在声明该匿名成员的包外,显式指定不可导出匿名成员的方式不被允许 。
  • 匿名成员不仅可以是结构体类型,任何命名类型或指向命名类型的指针都可以 ,这种机制是将简单类型组合成复杂复合类型的重要方式,是 Go 语言面向对象编程方式的核心 。

JSON

  • JSON 是一种用于发送和接收格式化信息的标准,是 JavaScript 值的 Unicode 编码,能表示字符串、数字、布尔值、数组和对象等 。它是基本数据类型和复合数据类型的高效、可读性强的表示方法 。JSON 的数组是元素序列,用逗号分隔、方括号括起;对象是键值对映射,键为字符串,用逗号分隔、花括号括起 。

Go 对 JSON 的支持

Go 通过标准库encoding/json等对 JSON 等格式提供编码和解码支持 。

  • 编码(Marshal ):将 Go 数据结构(如结构体、数组、slice、map 等 )转换为 JSON 格式称为marshal ,由json.Marshal函数实现 。如将movies[]Movie类型 )转换为 JSON ,json.Marshal生成字节 slice ,包含无多余空白字符的字符串 。json.MarshalIndent可输出整齐格式化的结果,通过设置前缀和缩进字符串实现 。结构体成员名作为 JSON 对象字段名(通过反射 ),可通过成员标签(field tag )指定对应 JSON 字段名及其他选项,如omitempty表示成员值为零值或空时不输出到 JSON 。
  • 解码(Unmarshal ):将 JSON 字符串解码为 Go 数据结构的过程叫unmarshal ,由json.Unmarshal实现 。合理定义 Go 数据结构,可选择将部分 JSON 数据解码到结构体对象,也可丢弃部分数据 。
// github.go
package github

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

const IssueURL = "https://api.github.com/repos/%s/issues/%d"

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 string `json:"created_at"`
	Body      string
}

type User struct {
	Login   string
	HTMLURL string `json:"html_url"`
}

func SearchIssues(terms []string) (*IssuesSearchResult, error) {
	q := url.QueryEscape(strings.Join(terms, " "))
	resp, err := http.Get(IssueURL + "?q=" + q)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("search query failed: %s", resp.Status)
	}

	var result IssuesSearchResult
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		resp.Body.Close()
		return nil, err
	}
	resp.Body.Close()

	return &result, nil
}
// issues.go
package main

import (
	"fmt"
	"log"
	"os"

	"gopl.io/ch4/github"
)

func main() {
	result, err := github.SearchIssues(os.Args)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d issues:\n", result.TotalCount)
	for _, item := range result.Items {
		fmt.Printf("#%-5d %9.9s %.55s\n", item.Number, item.User.Login, item.Title)
	}
}

image.png

  • 示例:以查询 GitHub 提供的 issue 跟踪接口为例,定义相关结构体(如IssuesSearchResultIssue等 ) ,通过SearchIssues函数发送 HTTP 请求获取 JSON 信息,并将其解析为 Go 结构体 。还可使用json.Decoder流解码方式依次从字节流解码多个 JSON 实体 ,并将结果格式化输出。

文本和HTML 模板

文本模板

  • 模板基础:当格式化需求复杂且需格式与代码分离时,可使用text/template包 。模板中用{{...}}包裹表达式 ,能实现输出值、选择结构体成员、调用函数方法、描述控制逻辑(如if - elserange循环 )等功能 。
  • 示例:以 GitHub 问题列表展示为例,通过模板输出符合条件的 issue 数量及每个 issue 的序号、用户、标题、创建时长等信息 。模板操作中,点号(. )表示当前值,{{.TotalCount}}输出总数,{{range .Items}}{{end}}创建循环 ,符号|用于操作间传递结果 ,如{{.Title | printf "%.64s"}}
  • 模板使用:使用模板需先解析再执行 。通过template.New创建模板,Funcs添加自定义函数(如daysAgo ) ,Parse解析模板 ,template.Must处理解析错误 ,最后Execute执行模板并输出结果 。

HTML 模板

  • 安全特性html/template包功能类似text/template ,但能自动转义 HTML、JavaScript、CSS 和 URL 中的字符串 ,避免注入攻击等安全问题 。
  • 示例展示:将 GitHub 问题列表以 HTML 表格形式输出 ,定义模板并解析执行 ,生成的 HTML 在浏览器中展示 。对比展示text/templatehtml/template对特殊字符处理的差异 ,html/template能正确转义 HTML 元字符 ,防止结构改变和安全风险 。
    printf “%.64s”}}` 。
  • 模板使用:使用模板需先解析再执行 。通过template.New创建模板,Funcs添加自定义函数(如daysAgo ) ,Parse解析模板 ,template.Must处理解析错误 ,最后Execute执行模板并输出结果 。

HTML 模板

  • 安全特性html/template包功能类似text/template ,但能自动转义 HTML、JavaScript、CSS 和 URL 中的字符串 ,避免注入攻击等安全问题 。
  • 示例展示:将 GitHub 问题列表以 HTML 表格形式输出 ,定义模板并解析执行 ,生成的 HTML 在浏览器中展示 。对比展示text/templatehtml/template对特殊字符处理的差异 ,html/template能正确转义 HTML 元字符 ,防止结构改变和安全风险 。

参考资料:《Go程序设计语言》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值