3. Go 数据类型及数据结构
前面的几节中有意无意地创建了很多变量,在变量声明过程中,除非声明过程就初始化,否则通常需要指明数据类型。不同的数据类型代表着不同的数据处理和表达方法,为了满足对现实世界的刻画要求,通常需要创建复杂的数据类型或结构,因为一个好的数据类型或结构可以恰到好处的解决所面临的问题,所以需要考虑数据结构的问题,但无论多么复杂的数据类型或结构,都是由简单的数据类型构建而来的,下面逐一进行熟悉。
Go 简单数据类型
Go 中简单数据类型大概有这么几类:
- 布尔型:
bool
,表示true
和false
- 数值型:
complex64
,complex128
,float32
,float64
,int
,int8
,int16
,int32
,int64
,int128
,unit
,unit8
,uint16
,uint32
,uint128
,表示各种大小长度的数字 - 字符串类型:
string
,表示字符串 - 指针:Pointer
- 杂项:
byte, rune
布尔型
布尔型,只有两种值,true
和 false
,但确实一种重要的表示状态的存在,布尔型变量主要进行逻辑运算,生成布尔型值的方式除了直接声明外,多数都是通过关系运算符获取的。
关系运算符,关系运算符用于可比较的两种类型的值进行比较,包括, ==, !=, > , >=, <, <=
-
==
检测两个值是否相等,相等返回 true,否则返回false
-
!=
检测两个值是否不相等,相等返回false
,否则返回true
-
>, >=
比较左端和右端两个值的大小,左端大于/大于等于右端的值,返回true
,否则返回false
-
<, <=
比较左端和右端两个值的大小,左端小于/小于等于右端的值,返回true
,否则返回false
逻辑运行,逻辑运算包括三种,与或非,对应符号为 &&, ||, !
两个/一个布尔型变量做上述运行时,与数学上的真值表是一致的。
-
x && y
结果要为true,要求x 为 true 并且 y 为 true
,其他均为false -
x || y
结果要为true,要求x 为 true 或者 y 为 true
,其他均为false -
!x
x为true
,!x
就是false
; 反之,x为false,!x
就是true
逻辑运算的优先顺序是 非,与,或,但是为了自己逻辑清楚,可以加小括号表示哪些关系密不可分。
注意:Go 中,true,false
并不能和 1, 0
进行默认转换,必须显式的进行转换。
func i2b(i int) bool {
if i > 0 {
return 1
}
return 0
}// int to bool
func b2i(b bool) int {
if b {
return 1
}
return 0
}// bool to int
数值型
Go 中数值型有两种,一种是没有任何长度标记的 int, uint
这两种,另一种就是有长度标记的数值,例如 int8, uint16
。前者的数据长度与平台相关,后者与平台无关,个人感觉是,如果你不排斥使用后者,那么你最好使用后者。
对于整型数据来说, uintx
的为无符号整型, intx
的为有符号整型,两者可以表达的数据范围是不一致的。
- uint8: 无符号 8 位整型 (0 到 255)
- uint16: 无符号 16 位整型 (0 到 65535)
- uint32: 无符号 32 位整型 (0 到 4294967295)
- uint64: 无符号 64 位整型 (0 到 18446744073709551615)
- int8: 有符号 8 位整型 (-128 到 127)
- int16 有符号 16 位整型 (-32768 到 32767)
- int32: 有符号 32 位整型 (-2147483648 到 2147483647)
- int64: 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
- float32:32 位浮点数 (很大)
- float64:64 位浮点数 (非常大)
- complex64:实部和虚部都是 float32 类型的的复数
- complex128:实部和虚部都是 float64 类型的的复数
对于数值型变量,最为常见的就是进行算术运算符,包括 +, -, *, /, %, ++, --, +=, *=, /=
-
%
整数取余,与被取模数的符号是一致的; -
x++, x--
自增和自减(没有++x,--x这样的东东); -
x+=y
就是x = x+y
的简写运算,其他几个类似;
除此之外, Go 还支持位运算
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 (AND NOT)
<< 左移
>> 右移
字符串
字符串就是字符的集合,更准确一点,是一个一旦创建完就不可改变的字节序列。之前提到,Go 语言字符串使用 UTF-8 编码实现,此处展开 UTF-8 不太合适,但需要知道 UTF-8 是一个变长编码,可以容纳世界上绝大多数语言及其字符。
字符串长度可以使用 len
来测量,字符串支持切片访问 string[index]
,字符串支持通过 +
对两个或多个字符串进行拼接。可以通过循环对一个字符串进行遍历。
package main
import "fmt"
func main() {
b := "Hello 中国"
fmt.Printf(b+", And have %d length.\n", len(b))
fmt.Println(b[0:10])
printStringHex(b)
printStringOnebyOne(b)
}
func printStringOnebyOne(s string) {
for index, rune := range s {
fmt.Printf("%c\t%x\t%d\n", rune, rune, index)
}
}
func printStringHex(s string) {
for i := 0; i < len(s); i++ {
fmt.Printf("%d\t%x\t%c\n", i, s[i], s[i])
}
}
/*
Hello 中国, And have 12 length.
Hello 中�
0 48 H
1 65 e
2 6c l
3 6c l
4 6f o
5 20
6 e4 ä
7 b8 ¸
8 ad
9 e5 å
10 9b �
11 bd ½
H 48 0
e 65 1
l 6c 2
l 6c 3
o 6f 4
20 5
中 4e2d 6
国 56fd 9
*/
注意对字符串遍历的方法,因为 UTF-8 变长编码的缘故,当处理英文字符,编码位置只有一个byte时,可以正常显示,但是对于中文,无法通过位置索引得到完整的一个字符,较好的稳妥的办法使用 range
来遍历,获取 rune。你也可能发现了,rune
是一个数据类型,它其实是 type rune int32, 是一个足够容纳编码位置的表达类型,按照如下方式访问字符串也是完全可以的。
func printChars(s string) {
runes := []rune(s)
for i:= 0; i < len(runes); i++ {
fmt.Printf("%c ",runes[i])
}
}
有一些回车符,Tab符号等不可见字符的表达需要使用转义字符,这一点同 C 语言一致。
\a 响铃
\b 退格
\f 换页
\n 换行
\r 回车
\t 制表符
\v 垂直制表符
\' 单引号 (只用在 '\'' 形式的rune符号面值中)
\" 双引号 (只用在 "..." 形式的字符串面值中)
\\ 反斜杠
为了处理大段文本(里面有很多换行,缩进之类的东西), Go 还支持一种字面字符串,它也是字符串,不同的是,它不用 ""
双引号表达,而是使用使用反引号代替双引号。
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
字符串是一种最为常见的数据类型,后面还会有专门的地方涉及到它。
指针
指针是一种直接存储变量的内存地址的数据类型。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
Go 的指针运算
& 返回变量存储地址,
// &a; 将给出变量的实际地址。
* 取出指针变量值,
// *a; 是一个取出该地址的值
注意 Go 的指针不允许指针的 ++,--
运算。 为什么要有指针呢,因为指针方便,同时也是为了降低开销。Go 在处理简单的数据类型时,数据之间的转移和传递是通过复制做到的,就是你把一个数据传递给另一个函数,那么 Go 会帮你复制一份数据过去,当遇到该数据特别大的时候,又要频繁传递的时候,就要进行大量的复制,影响性能。但用指针就不存在该问题,因为指针只保存指向某个数据的地址,如果需要,找那个地址的数据直接去改,不要反复复制了。
func main() {
var oneint int
var oneptr = &oneint
var anoptr *int = new(int) //*
fmt.Println(oneint)
oneint++
fmt.Println(*oneptr)
*oneptr += 2
fmt.Println(oneint)
fmt.Println(anoptr)
fmt.Println(*anoptr)
*anoptr = oneint
fmt.Println(*anoptr)
}
/* Result:
0 oneint 的默认值
1 oneint++
3 *oneptr += 2
0xc00000a090 //anoptr 地址
0 //*anoptr 的默认值
3 // *anoptr
*/
上面*标处,可利用自动推断为 var anoptr = new(int)
,此处利用new
分配内存空间,否则第四行输出为nil
,因为所有指针的默认值为nil
,就是空,没有指向任何地址。对 nil
地址操作 *anoptr = oneint
将引发panic,这是指针使用的大忌。
Go 类型之间转换
Go 有着非常严格的强类型特征。Go 没有自动类型提升或类型转换。
package main
import (
"fmt"
)
func main() {
i := 55 //int
j := 67.8 //float64
sum := i + j //不允许 int + float64
fmt.Println(sum)
}
那该怎么办……显式转换,全部需要显式转换
package main
import (
"fmt"
)
func main() {
i := 55 //int
j := 67.8 //float64
sum := i + int(j) //转换j为整数型,当然精度就没有了
fmt.Println(sum)
}
// 或者
func main() {
i := 55 //int
j := 67.8 //float64
sum :=j +float64(i) // 转换I到浮点数
fmt.Println(sum)
}
把一种类型转换为另一种类型的方法就是 T(v)
, T为新的类型,例如float64(i)
。
Go 符合数据类型
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
数组的声明:
var arrayname [n]type // n为确定的数字,type为简单的数据类型
var stringarray [3]string = [3]string{"Are", "Good", "Yor"}
// 最为繁重的声明方式,可以简化为 var stringarray = [3]string{"Are", "Good", "Yor"}
for index, value := range stringarray {
fmt.Printf("%d\t%s\n", index, value)
}
// range函数会遍历stringarray,返回两个值,第一个是索引,第二个是数值
/* Result
0 Are
1 Good
2 Yor
*/
var uintarray [5]uint
// 只声明,不初始化
uintarray[0] = 10
uintarray[4] = 100
for i, v := range uintarray {
fmt.Printf("%d\t%d\n", i, v)
}
/*Result
0 10
1 0
2 0
3 0
4 100
*/ 没有赋值的位置都是 0
floatarray := [...]float32{1:2.17,5: 3.14}
// 包括个数也自动推导
for _, v := range floatarray {
fmt.Printf("%g\n", v)
}
// 如果对索引位置不感兴趣,就用下划线去代替一个有意义的名称,这样就会丢弃掉那个值
/* Result
0 0
1 2.17
2 0
3 0
4 0
5 3.14
*/
数组的长度是不变的,在声明就已经指定,而且 Go 认为 [3]int, [4]int
是两种不同的数据类型,无法比较。初始化过程中,可以只初始化想初始化的位置,用{index:value}
来指定,如果{}
给定的值数量小于[]
指定的量,则认为初始化前面的几个数,后面为零。数组长度可以使用 len(array)
来获得,访问数据内的元素,可以通过 array[index]
,即数组名[索引位置]来访问,索引位置可以是单个数字,也可以是[start:end] 。数组是值类型,就是传递数组是通过传递数组的副本做到的。数组的遍历使用 for 下标索引或 range 索引都可以。
这里需要说一下下标的使用方法,一般来说 array[index]
是访问单元素, array[start:end]
是访问数组从 start
开始到 end-1
的那个元素,如果 start
不写,默认从 0 开始,如果 end
不写,默认到 len()-1
的位置,如果都不写,那就是 0:len()-1
也就是全部元素。
其实还可以创建多维数组,声明方法为 var mularray [row][vol]Type
来实现,遍历方式就是嵌套循环range,此处不再展开。
func print2darray(a [m][n]string) {
for row, rv := range a {
for vol, rvv := range rv {
fmt.Printf("%d\t%d\t%s\n",row,vol,rvv)
}
fmt.Printf("\n")
}
}
切片/Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个切片类型一般写作[]T,其中T代表切片中元素的类型;切片的语法和数组很像,只是没有固定长度而已。切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper)。切片本身不拥有任何数据。它们只是对现有数组的引用。一个切片有三个要素要关心,切片头(指针),长度(len),容量(cap)。
切片的创建有两种方式
- 从某个数组创建,
s := array[start:end] 0<= start < end <= len(array)-1
- 不关心数组,
s := make([]Type, len, cap) or s:=[]int{v1,v2,...,vn}
,也不是不想关心,是关心不到,反正能正常遍历各个值,为什么要关心数组呢。
先说第二种方式:
因为没有对照数组,单纯的说切片特性优良。当你创建一个切片后,你依然可以像数组一样,使用下标访问每个元素,使用 range 遍历整个切片。
既然说了可以变长,那就一定可以添加元素,使用 append 函数追加元素,格式为s = append(s, v1, v2, ...) or s = append(s, s1...)
就是说 append 函数可以在一个切片后面追加同类型的元素,或一个同类型的切片。当append
元素的数量超过原有切片的容量时,Go 会自动扩大容量,扩容多少,由一套复杂的算法决定,可不用关心。
从一个切片上,还可以继续创建子切片,subs := s[start:end]0<= start < end <= len(array)-1
,使用和切片相同。
理解第二种,就要回来说第一种了,有点复杂,看例子说明。
var uintarray [10]uint
uintarray[0] = 0
uintarray[2] = 100
uintarray[4] = 200
uintarray[6] = 300
uintarray[8] = 400
fmt.Println(uintarray, len(uintarray)) // [0 0 100 0 200 0 300 0 400 0] 10
uints := uintarray[1:3]
fmt.Println(uints, len(uints), cap(uints)) //[0 100] 2 9
fmt.Println(uints[0:3], len(uints[0:3]), cap(uints[0:3])) // [0 100 0] 3 9
fmt.Println(uints[1:4], len(uints[1:4]), cap(uints[1:4])) // [100 0 200] 3 8
uints[1] = 250
fmt.Println(uints[1:4], len(uints), cap(uints)) // [250 0 200] 3 8
uints[2] = 350 // panic,index out of range
uints = append(uints, 600, 700)
fmt.Println(uints, len(uints), cap(uints)) // [0 250 600 700] 4 9
fmt.Println(uintarray, len(uintarray)) // [0 0 250 600 700 0 300 0 400 0] 10
// 切片是对数组的引用,所以通过切片可以修改数组的值
uints = append(uints, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400)
fmt.Println(uints, len(uints), cap(uints))
// [0 250 600 700 600 700 800 900 1000 1100 1200 1300 1400] 13 18
fmt.Println(uintarray, len(uintarray))
//[0 0 250 600 700 0 300 0 400 0] 10
// 你不是说,修改切片,数组就相应变了吗!怎么这次没有变? 因为…… 因为这次添加的数太多了,超过切片容量,Go 重新创建了一个底层数组,复制了原有切片位置的值,添加新的值后返回了新的底层数组。而原有的数组被抛弃了,但我们有一个可以访问原有数组的变量存在,所以发现数没有变化。
ints := make([]int, 3, 3)
fmt.Println(ints, len(ints), cap(ints)) // [0 0 0] 3 3
aints := []int{1, 2, 3}
fmt.Println(aints, len(aints), cap(aints)) // [1 2 3] 3 3
sumints := append(ints, aints...)
fmt.Println(sumints, len(sumints), cap(sumints)) // [0 0 0 1 2 3] 6 6
subsumints := sumints[1:3]
fmt.Println(subsumints, len(subsumints), cap(subsumints)) //[0 0] 2 5
subsumints2 := sumints[:5]
fmt.Println(subsumints2, len(subsumints2), cap(subsumints2)) //[0 0 1 2 3] 5 5
// 创建的子切片长度比原有切片长度要大,但容量是一样
所以,一个切片一旦创建,无论何种方式创见的切片,其指针位置就确定了,指针前面的数据就永远不能访问了;在切片容量范围内,可以创建子切片,访问到由于容量所有数据;对于append 可以增加切片容量。切片对于底层数组是引用方式,修改切片就修改了底层数组,切片还有一种操作叫做 复制(copy),copy(dst, source)
从 source 复制数据到一个 dst 切片,如果想完整复制 source,则必须保证 dst 切片容量大于等于 source,否则只会复制dst容量的数据。
Map
Map有多种译名:字典,哈希表,集合......,其实都可以,因为 Map 是这样一种结构,非空的Map 里面是一系列键值对(键到值的映射),类似于一本字典;它的键是不允许重复的,所以它像一个集合;哈希表是 Map 的实现方式,它用哈希算法计算存储键,使得它可以对给定的key可以在常数时间复杂度内检索、更新或删除对应的value。由于不同的哈希算法生成的排序方式不同,所以Map是无序,更为特殊的,在 Go 中,它是故意被设计为乱序的,要求程序算法不能依赖于Map遍历的顺序。如果像排序,最好显式的获取Map的键,按照需要方式排序,然后再按照键去遍历。Map 是按照引用传递的,意思就是无论你在哪里修改一个Map,其都是对一个Map进行修改。Map 无法使用 ==
比较,除非是 MapName == nil
Map 声明
var mapname map[Ktype]Vtype{Key:Value}
var monthTable map[int]string
创建了一个从数字到月份名字的Map
var monthTable map[string]int
创建了一个从月份名字到数字的Map
如果没有利用{Key:Value}
初始化map, 则声明的Map默认值为 nil
。如果你想添加元素到 nil map 中,会触发运行时 panic。因此 map 必须使用 make
函数初始化。
初始化 monthTable = make(map[int]string)
,所以多数时候,声明和初始化是一起进行的,monthTable := make(map[int]string)
Map 的查询
Value,ok = MapName[key]
如果可以在给定的Map中查询到指定 key 的值,那么返回 value 和 true
,反之,返回值的默认零值和 false
.
Map 键值的添加
MapName[key] = value
如果 Map 初始化过,那么添加就是即可,如果没有初始化,panic报错
Map 键值的删除
delete(key, MapName)
如果Map 中存在该key,则删除,如果不存在,上述操作也是安全操作,最终结果就是一定没有该键。
package main
import (
"fmt"
)
func main() {
var StudentScore map[string]int
// var StudentScore map[int]string
fmt.Println(StudentScore, StudentScore == nil, len(StudentScore))
StudentScore = make(map[string]int)
fmt.Println(StudentScore, StudentScore == nil, len(StudentScore))
StudentScore["Xiaoming"] = 98
StudentScore["Xiaohong"] = 99
StudentScore["Xiaogang"] = 95
StudentScore["Xiaohei"] = 0
fmt.Println("Xiaoming Score:", StudentScore["Xiaoming"])
fmt.Println(StudentScore, StudentScore == nil, len(StudentScore))
for k, v := range StudentScore {
fmt.Println(k, ":", v)
}
fmt.Println("Xiaohei Score:", StudentScore["Xiaohei"])
fmt.Println("JackFly Score:", StudentScore["JackFly"])
lookupScore("Xiaohei", StudentScore)
lookupScore("JackFly", StudentScore)
setScore("JackFly", 100, StudentScore)
fmt.Println("JackFly Score:", StudentScore["JackFly"])
setScore("Xiaohei", 100, StudentScore)
fmt.Println("Xiaohei Score:", StudentScore["Xiaohei"])
delScore("Xiaohei", StudentScore)
setScore("Xiaohei", 99, StudentScore)
fmt.Println("Xiaohei Score:", StudentScore["Xiaohei"])
}
func lookupScore(s string, table map[string]int) {
Score, ok := table[s]
if ok {
fmt.Printf("%s Score: %d, ok Status:%t\n", s, Score, ok)
} else {
fmt.Printf("There is no %s Score in Table. ok Status:%t\n", s, ok)
}
}
func setScore(s string, score int, table map[string]int) {
if table == nil {
table = make(map[string]int)
}
Score, ok := table[s]
if ok {
fmt.Printf("%s already had a score:%d record in table.\n", s, Score)
} else {
table[s] = score
fmt.Printf("%s Score:%d had set in table successfully.\n", s, score)
}
}
func delScore(s string, table map[string]int) {
Score, ok := table[s]
if ok {
delete(table, s)
fmt.Printf("%s score:%d had been delete in table.\n", s, Score)
} else {
fmt.Printf("There is no %s score record in talbe.\n", s)
}
}
/*
map[] true 0
map[] false 0 // 初始化后就不是nil
Xiaoming Score: 98
map[Xiaohong:99 Xiaogang:95 Xiaohei:0 Xiaoming:98] false 4 // len可以给出map长度
Xiaoming : 98
Xiaohong : 99
Xiaogang : 95
Xiaohei : 0
Xiaohei Score: 0
JackFly Score: 0
Xiaohei Score: 0, ok Status:true
There is no JackFly Score in Table. ok Status:false
JackFly Score:100 had set in table successfully.
JackFly Score: 100
Xiaohei already had a score:0 record in table.
Xiaohei Score: 0
Xiaohei score:0 had been delete in table.
Xiaohei Score:99 had set in table successfully.
Xiaohei Score: 99
*/
结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的新数据类型,以满足描述算法或对象的使用描述要求,其中结构体的每个值称为结构体的成员。
结构体的定义与声明 , 结构体内成员的声明顺序是有意义的,不同顺序成员的结构体是不同的结构体。成员数据类型可以是前面的任意一种数据类型,甚至可以是另一个结构体(但不可以是自己)。
// 结构体定义
type StructName struct{
feildName1 TypeA
feildName2, feildName3 TypeB
feildName3, TypeC
} // 带有成员名称(字段)的定义方式
type StructName struct{
TypeA
TypeB
TypeC
} // 不带字段的定义方式
// 结构体声明
StructureV := StructureName{
feildName1: Value1,
feildName2: Value2,
feildName3: Value3,// 不一定要全部赋值,如果不赋值,Go 默认初始化为0
}
StructureV := StructureName{Value1, Value2, Value3}//由此注意结构体的声明成员的的顺序很重要,
创建匿名的结构体
StructureV :=struct{
feildName1 TypeA
feildName2, feildName3 TypeB
feildName3, TypeC
}{
feildName1: Value1,
feildName2: Value2,
feildName3: Value3,
}
结构体成员的访问
访问结构体的成员是通过.
来操作的,即 structV.fieldNameA
,用来读写都是可以的
结构体取地址及地址访问
结构体可以取地址,结构体的成员也可以单独取地址,通常通过地址访问成员,需要先对结构体使用 *
取值运算,但 Go 会自动对结构体地址进行取值 ,简化了结构体的表达,如下:structptr := &structName{}, (*structptr).fieldName 等价于 structptr.feildName
结构体内包含另一个结构体
structName{substructName, fnc typec}
时,如果另一个结构体substructName{fna typea, fnb typeb}
没有名称,那么Go 会提升字段,就是将substructName
中的成员,当成自己的成员,可以通过.
直接访问。但是对于有名称的结构体,则必须使用双重点号访问子结构体的成员。structName{sub substructName, fnc type}, structName.substructName.fna
结构体虽然没有办法包含自身在内,但是可以包含自身的指针在内。
package main
import (
"fmt"
)
type address struct {
city, road string
number int
Neighbor *address
}
type student struct {
Name string
gender bool // true for boy, false for girl
age int
hight float32
}
type studentDetail struct {
student
homeAddr address
}
func main() {
xiaoming := student{"xiaoming", true, 10, 1.1}
fmt.Println(xiaoming)
fmt.Printf("%+v\n", xiaoming)
printfstu(xiaoming)
xiaominginfo := &xiaoming
fmt.Println((*xiaominginfo).gender)
fmt.Println(xiaominginfo.age)
fmt.Println("A Year Later... ")
xiaominginfo.age++
xiaominginfo.hight = 1.2
printfstu(xiaoming)
xiaomingDetail := studentDetail{
student: xiaoming,
homeAddr: address{
city: "Beijing",
road: "ChangAn",
number: 1,
//Neighbor 是可以被省略的
},
}
fmt.Printf("%+v\n", xiaomingDetail)
printfstuD(&xiaomingDetail)
xiaohongDetail := &studentDetail{student{"xiaohong", false, 10, 1.1}, address{"Beijing", "Changan", 2, &xiaomingDetail.homeAddr}} // 地址不可省略
fmt.Println("xiaohong Age:", xiaohongDetail.age)
fmt.Println("xiaohong home address:", xiaohongDetail.homeAddr.road, xiaohongDetail.homeAddr.number)
fmt.Println("xiaohong Neighbor:", *xiaohongDetail.homeAddr.Neighbor)
}
func printfstu(stu student) {
gendermap := map[bool]string{
false: "girl",
true: "boy",
}
fmt.Printf("------------\n%s\nGender:%s\nAge:%d\nHight:%g\n", stu.Name, gendermap[stu.gender], stu.age, stu.hight)
}
func printfstuD(stuD *studentDetail) {
gendermap := map[bool]string{
false: "girl",
true: "boy",
}
fmt.Printf("------------\n%s\n - Gender: %s\n - Age: %d\n - Hight: %g\n - Address: %s, %s Road, No. %d\n", stuD.Name, gendermap[stuD.gender], stuD.age, stuD.hight, stuD.homeAddr.city, stuD.homeAddr.road, stuD.homeAddr.number)
}
由于这一节打印了很多不同类型的值,其格式化的方式都有所区别,可以通过这个 简单介绍 简单了解一下,后文会有机会全面理解。