数组
-
定义:数组是固定长度、元素数据类型相同的序列 。元素通过索引访问,索引从 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 是拥有相同类型元素的可变长度序列,写法为
[]T
,T
为元素类型,可看作底层数组的动态视图 。 -
底层结构:底层是数组,slice 有指针(指向数组第一个可访问元素 )、长度(可访问元素个数 )和容量(从第一个元素到底层数组最后一个元素间元素个数 )三个属性 ,用
len
和cap
函数获取长度和容量 。 -
与数组关系:一个底层数组可对应多个 slice ,slice 可引用数组任意位置,元素可重叠 。
-
创建新 slice:用
s[i:j]
(0 ≤ i ≤ j ≤ cap(s)
)操作符创建新 slice ,引用原 slice 从i
到j - 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]V
,K
为键类型,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
结构体包含ID
、Name
、Address
等成员 。 - 成员访问与操作:
- 通过点号(
.
)访问结构体成员,如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.Buffer
和sync.Mutex
。还提到空结构体struct{}
,无成员变量,长度为 0 ,可用于一些节省内存且语法简单的场景 ,如在map
中作值类型来替代集合 。
结构体字面量
-
第一种格式:按成员变量顺序为每个成员指定值 ,如
Point
结构体,p := Point{1, 2}
。此格式要求记住成员顺序,在结构体成员扩充或顺序调整时不利于代码维护,常用于结构体定义所在包或成员顺序有明显约定的小结构体 ,如image.Point
、color.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 == q
与p.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 绘图程序中形状库为例,最初定义Circle
和Wheel
结构体 ,Wheel
包含Circle
所有属性及额外的Spokes
属性 。随着形状增多,重构提取出Point
结构体,将Circle
定义为包含Center Point
和Radius
,Wheel
包含Circle
和Spokes
,但此时访问Wheel
成员变得繁琐 。
Go 语言允许定义不带名称的结构体成员(匿名成员 ),其类型须是命名类型或指向命名类型的指针 。如Circle
结构体中嵌入Point
,Wheel
结构体中嵌入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)
}
}
- 示例:以查询 GitHub 提供的 issue 跟踪接口为例,定义相关结构体(如
IssuesSearchResult
、Issue
等 ) ,通过SearchIssues
函数发送 HTTP 请求获取 JSON 信息,并将其解析为 Go 结构体 。还可使用json.Decoder
流解码方式依次从字节流解码多个 JSON 实体 ,并将结果格式化输出。
文本和HTML 模板
文本模板
- 模板基础:当格式化需求复杂且需格式与代码分离时,可使用
text/template
包 。模板中用{{...}}
包裹表达式 ,能实现输出值、选择结构体成员、调用函数方法、描述控制逻辑(如if - else
、range
循环 )等功能 。 - 示例:以 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/template
和html/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/template
和html/template
对特殊字符处理的差异 ,html/template
能正确转义 HTML 元字符 ,防止结构改变和安全风险 。
参考资料:《Go程序设计语言》