Kodewick
开始学 →
第 10 课,共 24 课目录

结构体

第 10 课,共 24 课 · 第 3 部分 · 类型 · 约 15 分钟

一件商品有名字、价格、库存。用三个变量也能写,可商品一多就乱了。结构体(struct)把相关的几个值放在一起,变成一个新的类型:

prog.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648	Stock int9}10 11func main() {12	lamp := Product{Name: "台灯", Price: 129.5, Stock: 3}13	fmt.Println(lamp)14	fmt.Println(lamp.Name, lamp.Price)15}
输出
{台灯 129.5 3}
台灯 129.5
部分是什么
type Product struct { … }定义一个叫 Product 的新类型,写在 main 外面。
Name string一个字段:名字在前,类型在后。
Product{Name: "台灯", …}结构体字面量:按名字给每个字段一个值。
lamp.Name用点取出一个字段。

fmt.Println 显示整个结构体时,把字段的值按顺序放在花括号里。字段名习惯用大写字母开头:大写开头的名字,别的包也能用;小写开头的只有自己这个包能用。

读写字段

字段用起来就像变量:可以读,也可以改。%+v 显示结构体时会带上字段名,查问题时很方便:

prog.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648	Stock int9}10 11func main() {12	lamp := Product{Name: "台灯", Price: 129.5, Stock: 3}13	lamp.Stock--14	lamp.Price = 9915	fmt.Println(lamp.Stock)16	fmt.Printf("%v\n", lamp)17	fmt.Printf("%+v\n", lamp)18}
输出
2
{台灯 99 2}
{Name:台灯 Price:99 Stock:2}

lamp.Stock-- 让库存减 1,和变量一样。Printf 第 2 课用过 %T;引号里还能写别的「格式」,常用的有这些:

格式显示成
%v默认的样子,和 Println 一样
%+v结构体带上字段名
%d整数
%s字符串
%q字符串,带上双引号
%.2f小数,保留两位(%.1f 是一位)
%T类型

写一个不存在的字段,编译器马上就发现了:

prog.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648	Stock int9}10 11func main() {12	lamp := Product{Name: "台灯", Price: 129.5, Stock: 3}13	fmt.Println(lamp.Color)14}
输出
# command-line-arguments
./prog.go:13:19: lamp.Color undefined (type Product has no field or method Color)

发生了什么: Product 没有叫 Color 的字段或方法(拼错了吗?)。

Product 没有叫 Color 的字段(也没有叫这个名字的方法,方法是第 12 课的内容)。拼错字段名也是这个错误。

零值

没给值的字段是它那种类型的零值:数是 0,字符串是 ""。用 var 声明的结构体,每个字段都是零值:

prog.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648	Stock int9}10 11type Point struct {12	X, Y int13}14 15func main() {16	var empty Product17	fmt.Printf("%+v\n", empty)18	pen := Product{Name: "笔"}19	fmt.Printf("%+v\n", pen)20	p := Point{3, 4}21	fmt.Println(p.X + p.Y)22}
输出
{Name: Price:0 Stock:0}
{Name:笔 Price:0 Stock:0}
7

X, Y int 是两个同类型的字段写在一行。Point{3, 4} 不写字段名,按顺序给值,这时每个字段都要给。字段一多就容易弄错,所以一般按名字写。

赋值是复制

把一个结构体赋给另一个变量,得到的是一整份复制。改了复制,原来的不变。点单步,看 a 和 b 是两个分开的格子:

prog.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648	Stock int9}10 11func main() {12	a := Product{Name: "台灯", Price: 129.5, Stock: 3}13	b := a14	b.Stock = 1015	fmt.Println(a.Stock, b.Stock)16}
输出
3 10

把结构体传给函数也一样:函数拿到的是一份复制,改它不影响调用的地方。想让函数改到原来的,要用指针,下一课讲。

结构体切片

很多件商品就放进 []Product。在花括号里写每个元素时,可以省掉类型名:

prog.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648	Stock int9}10 11func main() {12	items := []Product{13		{Name: "台灯", Price: 129.5, Stock: 3},14		{Name: "笔", Price: 4, Stock: 50},15		{Name: "本子", Price: 12, Stock: 10},16	}17	total := 0.018	for _, p := range items {19		total += p.Price * float64(p.Stock)20	}21	fmt.Println("库存总值", total)22}
输出
库存总值 708.5

range 给的 p 也是元素的一份复制。猜一猜:

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Stock int8}9 10func main() {11	items := []Product{{Name: "笔", Stock: 5}, {Name: "本子", Stock: 2}}12	for _, p := range items {13		p.Stock = 014	}15	fmt.Println(items[0].Stock, items[1].Stock)16}

按字段排序

sort.Slice 能按你想要的方式排序。除了切片,还要给它一小段函数,回答「第 i 个要不要排在第 j 个前面」(把函数当值传,第 16 课细讲):

prog.go
1package main2 3import (4	"fmt"5	"sort"6)7 8type Product struct {9	Name  string10	Price float6411}12 13func main() {14	items := []Product{15		{Name: "台灯", Price: 129.5},16		{Name: "笔", Price: 4},17		{Name: "本子", Price: 12},18	}19	sort.Slice(items, func(i, j int) bool {20		return items[i].Price < items[j].Price21	})22	for _, p := range items {23		fmt.Println(p.Name, p.Price)24	}25}
输出
笔 4
本子 12
台灯 129.5

从便宜到贵。把 < 换成 > 就是从贵到便宜;换成 items[i].Name < items[j].Name 就按名字排(汉字按编码比,不是按拼音或笔画)。

嵌入

字段只写类型、不写名字,叫嵌入:里面那个结构体的字段可以直接用。

prog.go
1package main2 3import "fmt"4 5type Address struct {6	City   string7	Street string8}9 10type Employee struct {11	Name string12	Address13}14 15func main() {16	e := Employee{Name: "小林", Address: Address{City: "上海", Street: "南京路 1 号"}}17	fmt.Println(e.City)18	fmt.Printf("%+v\n", e)19}
输出
上海
{Name:小林 Address:{City:上海 Street:南京路 1 号}}

e.City 就是 e.Address.City 的简写。%+v 显示出来,能看到 Address 是一个完整的字段。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

长方形的面积

写好 area,让它返回长方形的面积(宽乘高)。程序会显示两个长方形的面积。

要显示
12
30
exercise1.go
1package main2 3import "fmt"4 5type Rect struct {6	Width, Height int7}8 9func area(r Rect) int {10	return 011}12 13func main() {14	fmt.Println(area(Rect{Width: 3, Height: 4}))15	fmt.Println(area(Rect{Width: 5, Height: 6}))16}
输出
点「运行」或按 Ctrl+Enter
练习 2

最贵的商品

写好 mostExpensive,让它返回切片里价格最高的那件商品。程序会在两家店里各找一次。

要显示
台灯 129.5
水杯 25
exercise2.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price float648}9 10func mostExpensive(items []Product) Product {11	best := items[0]12	return best13}14 15func main() {16	shop := []Product{{Name: "笔", Price: 4}, {Name: "台灯", Price: 129.5}, {Name: "本子", Price: 12}}17	other := []Product{{Name: "勺子", Price: 8}, {Name: "水杯", Price: 25}}18	a := mostExpensive(shop)19	fmt.Println(a.Name, a.Price)20	b := mostExpensive(other)21	fmt.Println(b.Name, b.Price)22}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:补货没补上

每件商品应该补货 10 个,可程序显示的库存一个都没多。改好它,让它显示 笔 15 和 本子 12 两行。

要显示
笔 15
本子 12
exercise3.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Stock int8}9 10func main() {11	items := []Product{{Name: "笔", Stock: 5}, {Name: "本子", Stock: 2}}12	for _, p := range items {13		p.Stock += 1014	}15	for _, p := range items {16		fmt.Println(p.Name, p.Stock)17	}18}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • type 名字 struct { … } 定义结构体类型;字面量按名字写:Product{Name: "笔", Price: 4}。
  • 用点读写字段:p.Price = 99;没给值的字段是零值;%+v 显示时带字段名。
  • 结构体赋值、传给函数、range 拿到的,都是一整份复制。
  • 改切片里的结构体用下标:items[i].Stock += 10。
  • sort.Slice(s, func(i, j int) bool { … }) 按字段排序。
  • 嵌入:只写类型名的字段,里面的字段可以直接用。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。