结构体
一件商品有名字、价格、库存。用三个变量也能写,可商品一多就乱了。结构体(struct)把相关的几个值放在一起,变成一个新的类型:
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 显示结构体时会带上字段名,查问题时很方便:
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 | 类型 |
写一个不存在的字段,编译器马上就发现了:
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 声明的结构体,每个字段都是零值:
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 是两个分开的格子:
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。在花括号里写每个元素时,可以省掉类型名:
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}
每一轮的 p 是元素的复制,清零的是复制,切片里的元素没变。要改切片里的元素,得通过下标去改。
按字段排序
sort.Slice 能按你想要的方式排序。除了切片,还要给它一小段函数,回答「第 i 个要不要排在第 j 个前面」(把函数当值传,第 16 课细讲):
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 就按名字排(汉字按编码比,不是按拼音或笔画)。
嵌入
字段只写类型、不写名字,叫嵌入:里面那个结构体的字段可以直接用。
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 是一个完整的字段。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
长方形的面积
✓写好 area,让它返回长方形的面积(宽乘高)。程序会显示两个长方形的面积。
12 30
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}
最贵的商品
✓写好 mostExpensive,让它返回切片里价格最高的那件商品。程序会在两家店里各找一次。
台灯 129.5 水杯 25
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}
修好它:补货没补上
✓每件商品应该补货 10 个,可程序显示的库存一个都没多。改好它,让它显示 笔 15 和 本子 12 两行。
笔 15 本子 12
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}
这一课学会了
type 名字 struct { … }定义结构体类型;字面量按名字写:Product{Name: "笔", Price: 4}。- 用点读写字段:
p.Price = 99;没给值的字段是零值;%+v显示时带字段名。 - 结构体赋值、传给函数、
range拿到的,都是一整份复制。 - 改切片里的结构体用下标:
items[i].Stock += 10。 sort.Slice(s, func(i, j int) bool { … })按字段排序。- 嵌入:只写类型名的字段,里面的字段可以直接用。