课本: 《Go语言圣经》
helloworld.go
gopackage main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
Go是一门编译型语言
Go语言的工具链将源代码及其依赖转换成计算机的机器指令(静态编译)
Go语言原生支持Unicode
,它可以处理全世界任何语言的文本
go 简单命令使用
run
子命令: 编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。
直接输出结果
$ go run helloworld.go
Hello, 世界
build
子命令:保存编译结果
生成可执行的二进制文件(在 windowns 下为exe文件)
$ go build helloworld.go
生成 helloworld.exe文件
$ helloworld.exe
Hello, 世界
get 子命令:通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装
go get gopl.io/ch1/helloworld
go get {域名}/{作者或机构(远程包路径格式)}/{项目名}
程序结构
package
)组织:类型其他语言的库(libraries
)或者模块(modules
)
package
声明语句开始,这个例子里就是package main
import
导入包
main包
比较特殊。它定义了一个独立可执行的程序,而不是一个库
main包
里的main 函数
也很特殊,它是整个程序执行时的入口func
关键字、函数名、参数列表、返回值列表(这个例子里的main
函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成工具
gofmt
格式化代码
goimports
,可以根据代码需要,自动地添加或删除import
声明
需要安装
$ go get golang.org/x/tools/cmd/goimports
os
包以跨平台的方式,提供了一些与操作系统交互的函数和变量
os.Args
变量是一个字符串(string)的切片(slice)
os.Args[0],是命令本身的名字
其它的元素则是程序启动时传给它的参数
空标识符
(blank identifier),即_
(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。
不能用于自定义名字,只能在特定语法结构中使用
break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var
可以使用它们。在一些特殊的场景中重新定义它们也是有意义的。
内建常量: true false iota nil 内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error 内建函数: make len cap new append copy close delete complex real imag panic recover
比如:
named.go
gopackage main
import (
"fmt"
)
func main() {
var true string
true = "10"
fmt.Println(true)
}
$ gofmt -w named.go
$ go run named.go 10
声明 | 作用域 |
---|---|
函数内部定义 | 函数内部 |
函数外部定义 | 当前包的所有文件 |
函数外部定义(大写字母开头) | 外部的包访问 |
主要的四种声明
格式 | 说明 |
---|---|
var | 变量 |
const | 常量 |
type | 类型 |
func | 函数 |
语法:
var
变量名字 类型 = 表达式
“类型”或“= 表达式”两个部分可以省略其中的一个
省略类型信息:将根据初始化表达式来推导变量的类型信息
省略初始化表达式:将用零值初始化该变量
类型 | 零值 |
---|---|
数值类型 | 0 |
布尔类型 | false |
字符串类型 | "" |
接口或引用类型 (包括slice、指针、map、chan和函数) | nil |
数组或结构体等聚合类型 | 每个元素或字段都是对应该类型的零值 |
Go语言中不存在未初始化的变量
可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量
govar i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
初始化表达式可以是字面量或任意的表达式
包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到的时候完成初始化。
总结:
var 变量名 [类型] [=表达式] ,[]可以省略其中一个, 需要关注初始化0值,可以同时声明(+初始化)多个变量
函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量
名字 := 表达式
goanim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
i, j := 0, 1
“:=”是一个变量声明语句,而“=”是一个变量赋值操作
简短变量和 var 一样可以同时声明多个变量
简短变量声明左边的变量可能并不是全部都是刚刚声明的
简短变量声明语句中必须至少要声明一个新的变量
总结:
**:=与var几近等价,:=的左边只需要一个是新的变量即可,容易语义混乱
一个变量对应一个保存了变量对应类型值的内存空间。
通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字
如果用“var x int”声明语句声明一个x变量,那么 &x
表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int
,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。
*p
表达式对应p指针指向的变量的值gox := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
任何类型的指针的零值都是nil
返回函数中局部变量的地址也是安全的。
如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值
cs.go
gopackage main
import (
"fmt"
)
func main() {
var s int = 0
incr(&s)
fmt.Println(incr(&s))
var s2 int = 0
incr2(s2)
fmt.Println(incr2(s2))
}
// 使用指针直接操作对应地址
func incr(p *int) int {
*p++
return *p
}
func incr2(p int) int {
p++
return p;
}
$ go run cs.go 2 1
总结:
**&变量名 获取地址,指针名 获取指向的值, 类型 为对应的指针类型
另一个创建变量的方法是调用内建的new函数
*T
gop := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
总结:
用的少,初始化为0值,返回变量地址(指针)
变量的生命周期指的是在程序运行期间变量有效存在的时间段
变量 | 生命周期 | 说明 |
---|---|---|
包一级声明的变量 | 和整个程序的运行周期是一致的 | |
局部变量的生命周期 | 动态的 声明语句开始 不再被引用为止 存储空间可能被回收 | 函数的参数变量 和返回值变量都是局部变量 它们在函数每次被调用的时候创建 |
将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收
总结:
没啥好说的。
使用赋值语句可以更新一个变量的值
变量 = 新值表达式
变量 += 表达式
等价于
变量 = 变量 + 表达式
v : = 1
v++ // 等价方式 v = v + 1;v 变成 2 v-- // 等价方式 v = v - 1;v 变成 1
元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。
先计算右边所有表达式的值,然后统一更新左边的值
交换变量的值
goi, j = j, i x[i], x[j] = x[j],x[i]
求最大公约数(GCD)
gofunc gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
斐波那契数列(Fibonacci)
gofunc fib(n int) int {
x, y := 0, 1
for i := 0; i < n; i++ {
x, y = y, x+y
}
return x
}
元组赋值也可以使一系列琐碎赋值更加紧凑
goi, j, k = 2, 3, 5
左右数量必须对等。
总结:
先计算右边表达式,再赋值给左边,使用起来更加方便,可以交换变量
赋值语句是显式的赋值形式,除了显示赋值外,还存在隐式赋值
隐式赋值
函数出入参
复合类型的字面量
gomedals := []string{"gold", "silver", "bronze"}
//等价于
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
可赋值性
总结:
无
类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等
type
类型名字 底层类型
创建一个新的类型,其底层结构和底层类型一致
相同底层类型的类型并不相互兼容(包括条件判断都不兼容),只和其他未显示声明的数据兼容
类型声明语句一般出现在包一级
govar c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
类型为 Celsius 的 String() 函数,传入 c 可用
gofmt.Println(c.String())
例如:
tempconv.go
gopackage main
import "fmt"
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
type MyInt int
const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (t MyInt) String() string { return "MyInt:" + fmt.Sprintf("%d",t) }
func main() {
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
// fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
// fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
c = FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
var s MyInt = 1
fmt.Println(s)
fmt.Println(s.String())
}
$ go run tempconv.go 100 180 true true true 100°C 100°C 100°C 100°C 100 100 MyInt:1 MyInt:1
总结:
无
Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用
一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中
每个包都对应一个独立的名字空间
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息
包级别的常量名都是以大写字母开头,可以被外部访问
每个源文件的包声明前紧跟着的注释是包注释
注意:
$ go build 文件目录
使用 go build 编译包
文件目录为 $GOPATH/src/[目录]
如:
# $GOPATH/src/test/pk 使用以下进行编译
$ go build test/pk
一般目录后缀和包名一致(约定俗成,可以不一致)
总结:
无
每个包都有一个全局唯一的导入路径
import
( "GOPATH下src的相对目录"
)
例子
$GOPATH/src/test/pk/t1.go
go/*
注释:xxxx
*/
package yuitest
const (
Test1 string = "Test1"
)
func t1() string { return "t1" }
func T1Export() string { return "t1 export" }
$GOPATH/src/test/pk/t2.go
gopackage yuitest
func t2() string { return "t2" }
func T2Export() string { return "t2 export" }
$GOPATH/study/ch2/testpackage.go
gopackage main
import (
"fmt"
"test/pk"
)
func main() {
fmt.Println(yuitest.Test1)
fmt.Println(yuitest.T1Export())
fmt.Println(yuitest.T2Export())
}
$ go run testpackage.go Test1 t1 export t2 export
这个例子里面,目录名和包名并没有一致
如果一个目录放不同包导入会编译异常
go$ go build test/pk
can't load package: package test/pk: found packages yuitests (s1.go) and yuitest
(t1.go) in D:\ProjectsOfGolang\src\test\pk
总结:
无
包的初始化首先是解决包级变量的依赖顺序
govar a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
包中含有多个.go源文件,将.go文件根据文件名排序,然后依次调用编译器编译。
包级别声明的变量初始化表达式则用表达式初始化,或用init初始化函数
每个文件都可以包含多个init初始化函数
func init() { /* ... */ }
init初始化函数除了不能被调用或引用
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。
main包最后被初始化
总结:
按依赖顺序初始化,先初始化包级变量,调用 init 函数,最后才是 main 包
声明语句的作用域是指源代码中可以有效使用这个名字的范围。
Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。
基础类型,包括:数字、字符串和布尔型
数值类型包括几种不同大小的整数、浮点数和复数
数据类型 | 大小(bit) | 无符号表示 | 说明 |
---|---|---|---|
int8 | 8 | uint8 | -- |
int16 | 16 | uint16 | -- |
int32 | 32 | uint32 | -- |
int64 | 64 | uint64 | -- |
int | 32或64 | uint | 不同的编译器,不同的平台 不一样 |
rune | 32 | -- | 等价于 int32 通常用于表示一个Unicode码点 |
byte | 8 | -- | uint8类型的等价类型 一般用于强调数值是 一个原始的数据而不是 一个小的整数 |
uintptr | 足以容纳指针 | -- | 只有在底层编程时才需要 |
int、uint和uintptr是不同类型的兄弟类型
有符号整数采用2的补码形式表,最高bit位用来表示符号位
有符号数取值范围为 :
无符号取值范围为
text* / % << >> & &^ + - | ^ == != < <= > >= && ||
=
好结合,简化赋值语句+
、-
、*
和/
可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算**即使数值本身不可能出现负数,也倾向于使用有符号的int类型,**就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择
0
开头书写,如:0666;通常用于POSIX操作系统上的文件访问权限标志0x
或 0X
开头书写,如 0xdef;强调数字值的bit位模式goo := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
fmt 在默认情况下 % 对应输出对应位参数
%[n] n代表指定操作数,从1开始
%# #表示带着前缀,0、0x、0X 输出
数据类型 | 大小(bit) | 极限值 | 说明 |
---|---|---|---|
float32 | 32 | math.MaxFloat32 | 约6个十进制数的精度 |
float64 | 64 | math.MaxFloat64 | 约15个十进制数的精度 |
通常应该优先使用float64类型
%g
参数打印浮点数,更紧凑的表示形式%e
(带指数)或%f
的形式打印可能更合适gofor x := 0; x < 8; x++ {
fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
output:
x = 0 e^x = 1.000 x = 1 e^x = 2.718 x = 2 e^x = 7.389 x = 3 e^x = 20.086 x = 4 e^x = 54.598 x = 5 e^x = 148.413 x = 6 e^x = 403.429 x = 7 e^x = 1096.633
数据类型
数据类型 | 大小 | 说明 |
---|---|---|
complex64 | 64 | 对应float32 |
complex128 | 128 | 对应float64 |
内置的complex函数用于构建复数
内建的real和imag函数分别返回复数的实部和虚部
govar x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
一个浮点数面值或一个十进制整数面值后面跟着一个i,将构成一个复数的虚部,复数的实部是0:
简化声明
gox := 1 + 2i // 1+2i
y := 3 + 4i // 3+4i
复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的
一个布尔类型的值只有两种:true和false。
!
: !true = false=
和<
等比较操作会产生布尔型的值&&
的优先级比||
高 (&&
对应逻辑乘法,||
对应逻辑加法,乘法比加法优先级要高)一个字符串是一个不可改变的字节序列
len函数
可以返回一个字符串中的字节数目s[i]
返回第i个字节的字节值,i必须满足0 ≤ i < len(s)
条件约束。(第i个字节并不一定是字符串的第i个字符)s[i:j]
基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串,新字符串将包含j-i
个字节+
操作符将两个字符串连接构造一个新字符串==
和<
进行比较
字符串值也可以用字符串面值方式编写
"Hello world"
转义字符
text\a 响铃 \b 退格 \f 换页 \n 换行 \r 回车 \t 制表符 \v 垂直制表符 \' 单引号(只用在 '\'' 形式的rune符号面值中) \" 双引号(只用在 "..." 形式的字符串面值中) \\ 反斜杠
进制
text十六进制以 \xhh 形式标识,h 代表十六进制数,大小写都可,如 \xDf 八进制 \ooo 形式表示,只有三位,不可以超过 \377
原生字符串
反引号代替双引号
go`
在原生的字符串面值中,没有转义操作;
全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行
原生字符串面值中无法使用符号
可以用八进制或十六进制转义或+"`"连接字符串常量完成
`
text0xxxxxxx runes 0-127 (ASCII) 110xxxxx 10xxxxxx 128-2047 (values <128 unused) 1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
\uhhhh
对应16bit的码点值,\Uhhhhhhhh
对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。\x41
对应字符'A',但是对于更大的码点则必须使用\u
或\U
转义形式。
总结:
Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储
UTF-8 是 Unicode 的实现方式之一
标准库中有四个包对字符串处理尤为重要:bytes
、strings
、strconv
和unicode
包。
strings
包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。bytes
包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型strconv
包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换unicode
包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类字符串和字节slice之间可以相互转换:
Gos := "abc"
b := []byte(s)
s2 := string(b)
整数转为字符串
gox := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
%b
、%d
、%o
和%x
等参数提供功能往往比strconv包的Format函数方便很多常量表达式的值在编译期计算,而不是在运行期。
每种常量的潜在类型都是基础类型:boolean
、string
或 数字。
常量的值不可修改
语法
const
<name> [type] = <value>或
const
( <name1> [type] = <value1> <name2> [type]= <value2>
)
// 类型可以省略
常量可以是构成类型的一部分,如指定数组长度
没有显式指明类型,那么将从右边的表达式推断类型
批量声明时,第一个常量不可以省略初始化,其他的如果省略初始化则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的
常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。
在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一
gopackage main
import "fmt"
type Weekday int
const (
t1 = 1
t2
s1 Weekday = iota
s2
s3
s4
)
const (
ss1 Weekday = iota
ss2
ss3
ss4
)
func main() {
fmt.Println(s1, s2, s3, s4)
fmt.Println("----------------------")
fmt.Println(ss1, ss2, ss3, ss4)
}
$ go run testConst.go
2 3 4 5
----------------------
0 1 2 3
iota 可以理解为第n-1常量,可以再表达式中使用
goconst (
t1 = 2 * iota //0
t2 //2
t3 //4
)
编译器为常量提供比基础类型更高精度的算术运算(256bit+的运算精度)
主要学习四种复合类型:数组、slice、map和结构体
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成
数组的长度是固定的
数组的每个元素可以通过索引下标来访问
内置的len函数将返回数组中元素的个数
顺序初始化值
go// 声明初始化
var q [3]int = [3]int{1, 2, 3}
// 字面值初始化中,由字面值数量决定数组长度
q1 := [...]int{1,2,3}
数组的长度必须是常量表达式
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的
Printf 函数的%x
副词参数,它用于指定以十六进制的格式打印数组或slice全部的元素
%T
副词参数是用于显示一个值对应的数据类型%t
副词参数是用于打印布尔型数据函数入参是原数据的副本,所以传入较大的数组是效率会比较低下
可以通过指针进行传递
gopackage main
import "fmt"
func main() {
var t1 [3]int = [3]int{1, 2, 3}
fmt.Println(t1)
change1(t1)
fmt.Println(t1)
change2(&t1)
fmt.Println(t1)
}
func change1(data [3]int) {
data[1] = 100
fmt.Println(data)
}
func change2(data *[3]int) {
(*data)[1] = 100
fmt.Println(*data)
}
$ go run testArray.go [1 2 3] [1 100 3] [1 2 3] [1 100 3] [1 100 3]
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。
gopackage main
import "fmt"
func main() {
q := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s := q[1:4]
fmt.Println(s)
fmt.Println(len(s))
fmt.Println(cap(s))
s1 := s[:4]
fmt.Println(s1)
}
$ go run testSlice.go [2 3 4] 3 8 [2 3 4 5]
通常是将append返回的结果直接赋值给输入的slice变量
实际上对应任何可能导致长度、容量或底层数组变化的操作重新把输出结果赋值给变量都是必要的。
注意:
尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的
一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value
K对应的key必须是支持==比较运算符的数据类型
内置的make函数可以创建一个map
goages := make(map[string]int) // mapping from strings to ints
map字面值的语法创建map
goages := map[string]int{
"alice": 31,
"charlie": 34,
}
// 等价于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
创建空的map的表达式:
gomap[string]int{}
Map中的元素通过key对应的下标语法访问
使用内置的delete函数可以删除元素:
godelete(ages, "alice") // remove element ages["alice"]
查找失败将返回value类型对应的零值
x += y
和x++
等简短赋值语法也可以用在map
goages["bob"] += 1
ages["bob"] ++
对于 number 类型来讲,需要区分是存在的 0 和非存在的 0(nil),可以如下处理
goage, ok := ages["bob"]
if !ok {/* "bob" is not a key in this map; */}
map中的元素并不是一个变量,不能对map的元素进行取址操作
go_ = &ages["bob"] // compile error: cannot take address of map element
遍历map中全部的key/value
可以使用 range 风格的for循环实现
gofor name, age := range ages {
fmt.Printf("%s\t%d\n", name, age)
}
map 强制每次遍历都是不同顺序的
要排序时,需要使用 key 去生成一个 slice 去排序,在去调用
goimport "sort"
var names := make([]string, 0, len(ages))
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
注意:
map 下标语法(
map[xxx]
)会产生两个返回值,第一个为value值(不存在key时,会返回类型零值),第二个是一个布尔值,用于报告元素是否真的存在key 必须是可以比较的
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。
gotype <StructName> struct {
<filedName> <fildeType>
<filedName> <fildeType>
<filedName> <fildeType>
...
}
var <name> <StructName>
结构体变量的成员可以通过点操作符访问: name.fileName
点操作符也可以和指向结构体的指针一起工作
govar t1 *StructName = &name
t1.fileName += "test"
// 等价
(*t1).fileName += "test"
调用函数返回的是值,并不是一个可取地址的变量
结构体成员的输入顺序也有重要的意义,不同的输入顺序构成不同的结构体
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S
指针类型的成员,这可以让我们创建递归的数据结构
结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。
Gotype Point struct{ X, Y int }
p := Point{1, 2}
上面是一种声明方式但是结构体成员有细微的调整就可能导致上述代码不能编译,因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则
Goanim := gif.GIF{LoopCount: nframes}
声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员
Gotype Point struct {
X, Y int
}
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
Gotype Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20
// 字面值不能使用简短声明
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
Gofunc name(parameter-list) (result-list) {
body
}
Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题
一个函数可以返回多个值
调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量
如果某个值不被使用,可以将其分配给blank identifier:
Golinks, _ := findLinks(url) // errors ignored
一个函数内部可以将另一个有多返回值的函数调用作为返回值
如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
gofunc test() (i int, err string, isSuccess bool){
i = 10
err = "err"
isSuccess = true
// 等价于 return i, err, isSuccess
return
}
函数被看作第一类值(first-class values)
go func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // "9"
f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"
f = product // compile error: can't assign func(int, int) int to func(int) int
Go var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误
Go var f func(int) int
if f != nil {
f(3)
}
testFunc.go
gopackage main
import "fmt"
func main() {
f := func(x int) string { fmt.Println(x, "次"); return "success" }
f(1)
f(2)
fmt.Println(testFunc(f))
}
func testFunc(print func(x int) string) string {
return print(123)
}
$ go run testFunc.go
1 次 2 次 123 次 success
在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数
slice
数组
数组
,在把 数组
的 slice
传递给函数使用testFunc.go
gopackage main
import "fmt"
func main() {
testParams(1,2,3,4)
data := [4]int{2,2,3,4}
testParams(data[:]...)
dataSlice := []int{3,2,3,4}
testParams(dataSlice...)
}
func testParams(t1 ...int) {
fmt.Println(t1)
}
$ go run testFunc.go
[1 2 3 4] [2 2 3 4] [3 2 3 4]
**延迟函数,延迟调用 defer
声明的语句 **
在函数结束时执行,不管是 reutrn 结束的,还是 panic 结束
一般用以关闭 io 等系统资源
也常被用于记录何时进入和退出函数
testDefer.go
gofunc testDefer(num ...int){
result := 0
defer fmt.Println(result)
for _, value := range num {
result += value
}
fmt.Println(result)
}
testDefer(1,2,3,4)
$ go run testDefer.go
10 0
进入和退出的记录
gofunc bigSlowOperation() {
// trace方法后面的()是必须的,如果没有,结束的时候才执行 start 的打印,而 exit 的打印不会被执行
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}
由于panic会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致
defer
内容,结束函数调用panic("异常信息")
gofunc Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}
函数声明时,在其名字前放上一个变量,即是一个方法
testMethod.go
gopackage main
import "fmt"
type Cat struct {
Age int
Name string
}
func (cat Cat) call() {
fmt.Printf("喵喵喵~,我是 %s, 今年 %d 岁了\n", cat.Name, cat.Age)
}
type Cats []Cat
func (cats Cats) call() {
for _, cat := range cats {
cat.call()
}
}
func main() {
cat := Cat{Age: 10, Name: "小花"}
cat.call()
cats := Cats{
{9, "红猫"},
{11, "蓝猫"},
}
cats.call()
}
$ go run testMethod.go
喵喵喵~,我是 小花, 今年 10 岁了
喵喵喵~,我是 红猫, 今年 9 岁了 喵喵喵~,我是 蓝猫, 今年 11 岁了
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了
gopackage main
import "fmt"
type Cat struct {
Age int
Name string
}
func (cat *Cat) call() {
// golang 隐式的转换为 (*cat).Name, (*cat).Age
fmt.Printf("喵喵喵~,我是 %s, 今年 %d 岁了\n", cat.Name, cat.Age)
}
func main() {
cat := Cat{Age: 10, Name: "小花"}
cat.call() // golang 隐式的转换为 (&cat).call(),也可以显示的去写
}
go// 函数声明
func name(paramName Type) returnType {}
// 匿名函数
f := func (paramName Type) returnType {}
f(data)
// 函数作为入参
func test(fnc func(paramName Type) returnType, data Type) {
fnc(data)
}
test(f)
// 延迟函数
// 类似函数级的 finally 不同的是调用函数返回的函数,能达到先调用函数,函数结束时调用函数返回的函数,一次达到进出记录和调用的效果
defer funcCall()() // funcCall() 返回一个函数,funcCall()执行到这条语句时先被调用,所处函数结束后调用返回函数
// 异常
panic("异常")
// 异常捕获,声明在 defer 中
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
本文作者:Yui_HTT
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!