20220616 Go 语言 - 菜鸟教程
安装环境
安装包下载地址为:https://golang.org/dl/
如果打不开可以使用这个地址:https://golang.google.cn/dl/
Windows 系统下安装
默认情况下 .msi 文件会安装在 c:\Go 目录下。你可以将 c:\Go\bin 目录添加到 Path 环境变量中。添加后你需要重启命令窗口才能生效。
# 验证安装成功
>go version
go version go1.17.7 windows/amd64
test.go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
>go run test.go
Hello, World!
Go 语言结构
Go 语言的基础组成有以下几个部分:
- 包声明
- 引入包
- 函数
- 变量
- 语句 & 表达式
- 注释
以上面的 test.go
为例进行分析,
package main
表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为main
的包fmt
包实现了格式化 IO(输入/输出)的函数func main()
是程序开始执行的函数。main
函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有init()
函数则会先执行该函数)/*...*/
是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以//
开头的单行注释。多行注释也叫块注释,均已以/*
开头,并以*/
结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段fmt.Println(...)
可以将字符串输出到控制台,并在最后自动增加换行字符\n
。使用fmt.Print("hello, world\n")
可以得到相同的结果。
Print
和Println
这两个函数也支持使用变量,如:fmt.Println(arr)
。如果没有特别指定,它们会以默认的打印格式将变量arr
输出到控制台- 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:
Group1
,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的public
);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的protected
)
注意:{
不能单独放在一行
关于包,根据本地测试得出以下几点:
- 文件名与包名没有直接关系,不一定要将文件名与包名定成同一个
- 文件夹名与包名没有直接关系,并非需要一致
- 同一个文件夹下的文件只能有一个包名,否则编译报错
基础语法
Go 标记
Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:
fmt.Println("Hello, World!")
行分隔符
在 Go 程序中,一行代表一个语句结束。每个语句不需要以分号 ;
结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ;
人为区分,但在实际开发中我们并不鼓励这种做法。
标识符
标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母( A~Z 和 a~z )数字( 0~9 )、下划线 _
组成的序列,但是第一个字符必须是字母或下划线而不能是数字。
以下是有效的标识符:
mahesh kumar abc move_name a_123
myname50 _temp j a23b9 retVal
以下是无效的标识符:
1ab
(以数字开头)case
(Go 语言的关键字)a+b
(运算符是不允许的)
字符串连接
Go 语言的字符串可以通过 +
实现:
package main
import "fmt"
func main() {
fmt.Println("Google" + "Runoob")
}
Go 语言的空格
Go 语言中变量的声明必须使用空格隔开
var age int;
在变量与运算符间加入空格,程序看起来更加美观
格式化字符串
Go 语言中使用 fmt.Sprintf 格式化字符串并赋值给新串:
package main
import (
"fmt"
)
func main() {
// %d 表示整型数字,%s 表示字符串
var stockcode = 123
var enddate = "2020-12-31"
var url = "Code=%d&endDate=%s"
var targetUrl = fmt.Sprintf(url, stockcode, enddate)
fmt.Println(targetUrl)
}
输出结果为:
Code=123&endDate=2020-12-31
数据类型
在 Go 编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
Go 语言按类别有以下几种数据类型:
数据类型 | 描述 |
---|---|
布尔型 | 布尔型的值只可以是常量 true 或者 false 。一个简单的例子:var b bool = true |
数字类型 | 整型 int 和浮点型 float32 、float64 ,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码 |
字符串类型 | 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本 |
派生类型 | 指针类型(Pointer) 数组类型 结构化类型(struct) Channel 类型 函数类型 切片类型 接口类型(interface) Map 类型 |
变量
变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。
变量可以通过变量名访问。
Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。
声明变量的一般形式是使用 var 关键字:
var identifier type
可以一次声明多个变量:
var identifier1, identifier2 type
示例:
package main
import "fmt"
func main() {
var a string = "Runoob"
fmt.Println(a)
var b, c int = 1, 2
fmt.Println(b, c)
}
输出结果:
Runoob
1 2
变量声明
第一种,指定变量类型,如果没有初始化,则变量默认为零值。
var v_name v_type
v_name = value
零值就是变量没有做初始化时系统默认设置的值。
-
数值类型(包括 complex64/128 )为
0
-
布尔类型为
false
-
字符串为
""
(空字符串) -
以下几种类型为
nil
:var a *int var a []int var a map[string] int var a chan int var a func(string) int var a error // error 是接口
var i int
var f float64
var b bool
var s string
var a map[string] int
fmt.Printf("%v %v %v %q\n", i, f, b, s) // 0 0 false ""
fmt.Println(a) // map[]
第二种,根据值自行判定变量类型。
var v_name = value
var d = true
fmt.Println(d) // true
第三种,如果变量已经使用 var 声明过了,再使用 :=
声明变量,就产生编译错误
v_name := value
:=
是一个声明语句
错误示例:
var intVal int
intVal :=1 // 这时候会产生编译错误,因为 intVal 已经声明,不需要重新声明
intVal := 1
相等于:
var intVal int
intVal = 1
多变量声明
//类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断
vname1, vname2, vname3 := v1, v2, v3 // 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误
// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
示例:
package main
var x, y int
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a int
b bool
)
var c, d int = 1, 2
var e, f = 123, "hello"
//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"
func main() {
g, h := 123, "hello"
println(x, y, a, b, c, d, e, f, g, h) // 0 0 0 false 1 2 123 hello 123 hello
}
值类型和引用类型
所有像 int
、float
、bool
和 string
这些基本类型都属于 值类型 ,使用这些类型的变量直接指向存在内存中的值
当使用等号 =
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是 在内存中将 i
的值进行了拷贝
可以通过 &i
来获取变量 i
的内存地址
值类型变量的值存储在堆中。
更复杂的数据通常会需要使用多个字,这些数据一般使用 引用类型 保存。
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
这个内存地址称之为 指针 ,这个指针实际上也被存在另外的某一个值中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 r2 = r1
时,只有引用(地址)被复制。
如果 r1
的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2
也会受到影响。
简短形式,使用 :=
赋值操作符
我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,声明语句写上 var
关键字其实是显得有些多余了,因此我们可以将它们简写为 a := 50
或 b := false
。
a
和 b
的类型( int
和 bool
)将由编译器自动推断。
这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 :=
可以高效地创建一个新的变量,称之为初始化声明。
注意事项
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20
就是不被允许的,编译器会提示错误 no new variables on left side of :=
,但是 a = 20
是可以的,因为这是给相同的变量赋予一个新的值。
如果你在定义变量 a
之前使用它,则会得到编译错误 undefined: a
。
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误
但是全局变量是允许声明但不使用的。 同一类型的多个变量可以声明在同一行,如:
var a, b, c int
多变量可以在同一行进行赋值,如:
var a, b int
var c string
a, b, c = 5, 7, "abc"
上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用:
a, b, c := 5, 7, "abc"
这被称为 并行 或 同时 赋值。
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a
,两个变量的类型必须是相同。
空白标识符 _
也被用于抛弃值,如值 5
在:_, b = 5, 7
中被抛弃。
_
实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val
和错误 err
是通过调用 Func1
函数同时得到:val, err = Func1(var1)
常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:
const identifier [type] = value
可以省略类型说明符 [type]
,因为编译器可以根据变量的值来推断其类型。
多个相同类型的声明可以简写为:
const c_name1, c_name2 = value1, value2
示例:
package main
import "fmt"
func main() {
const LENGTH int = 10
const WIDTH int = 5
var area int
const a, b, c = 1, false, "str" //多重赋值
area = LENGTH * WIDTH
fmt.Printf("面积为 : %d", area)
println()
println(a, b, c)
}
常量还可以用作枚举:
const (
Unknown = 0
Female = 1
Male = 2
)
数字 0、1 和 2 分别代表未知性别、女性和男性。
常量可以用 len()
, cap()
, unsafe.Sizeof()
函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过
package main
import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)
func main() {
println(a, b, c) // abc 3 16
}
iota
iota
,特殊常量,可以认为是一个可以被编译器修改的常量。
iota
在 const
关键字出现时将被重置为 0
(const
内部的第一行之前),const
中每新增一行常量声明将使 iota
计数一次(iota
可理解为 const
语句块中的行索引)。
第一个 iota
等于 0
,每当 iota
在新的一行被使用时,它的值都会自动加 1
iota 可以被用作枚举值
const (
a = iota
b = iota
c = iota
)
// 可简写为:
const (
a = iota
b
c
)
示例:
package main
const (
a = iota
b
c
)
func main() {
println(a, b, c) // 0 1 2
}
iota
用法
package main
import "fmt"
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a, b, c, d, e, f, g, h, i) // 0 1 2 ha ha 100 100 7 8
}
package main
import "fmt"
const (
i = 1 << iota
j = 3 << iota
k
l
)
func main() {
fmt.Println("i=", i) // i= 1 1左移0位
fmt.Println("j=", j) // j= 6 3左移1位
fmt.Println("k=", k) // k= 12 3左移2位
fmt.Println("l=", l) // l= 24 3左移3位
}
运算符
运算符用于在程序运行时执行数学或逻辑运算。
Go 语言内置的运算符有:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 其他运算符
算术运算符
package main
import "fmt"
func main() {
var a int = 21
var b int = 10
var c int
c = a + b
fmt.Printf("第一行 - c 的值为 %d\n", c) // 31
c = a - b
fmt.Printf("第二行 - c 的值为 %d\n", c) // 11
c = a * b
fmt.Printf("第三行 - c 的值为 %d\n", c) // 210
c = a / b
fmt.Printf("第四行 - c 的值为 %d\n", c) // 2
c = a % b
fmt.Printf("第五行 - c 的值为 %d\n", c) // 1
a++
fmt.Printf("第六行 - a 的值为 %d\n", a) // 22
a = 21 // 为了方便测试,a 这里重新赋值为 21
a--
fmt.Printf("第七行 - a 的值为 %d\n", a) // 20
}
关系运算符
package main
import "fmt"
func main() {
var a int = 21
var b int = 10
if a == b {
fmt.Printf("第一行 - a 等于 b\n")
} else {
fmt.Printf("第一行 - a 不等于 b\n") // true
}
if a < b {
fmt.Printf("第二行 - a 小于 b\n")
} else {
fmt.Printf("第二行 - a 不小于 b\n") // true
}
if a > b {
fmt.Printf("第三行 - a 大于 b\n") // true
} else {
fmt.Printf("第三行 - a 不大于 b\n")
}
/* Lets change value of a and b */
a = 5
b = 20
if a <= b {
fmt.Printf("第四行 - a 小于等于 b\n") // true
}
if b >= a {
fmt.Printf("第五行 - b 大于等于 a\n") // true
}
}
逻辑运算符
package main
import "fmt"
func main() {
var a bool = true
var b bool = false
if a && b {
fmt.Printf("第一行 - 条件为 true\n")
}
if a || b {
fmt.Printf("第二行 - 条件为 true\n") // true
}
/* 修改 a 和 b 的值 */
a = false
b = true
if a && b {
fmt.Printf("第三行 - 条件为 true\n")
} else {
fmt.Printf("第三行 - 条件为 false\n") // true
}
if !(a && b) {
fmt.Printf("第四行 - 条件为 true\n") // true
}
}
位运算符
位运算符对整数在内存中的二进制位进行操作
| p
| q
| p & q
| p | q
| p ^ q
|
| :--- | :--- | :------ | :------ | :------ |
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
假定 A = 60; B = 13; 其二进制数转换为:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
示例:
package main
import "fmt"
func main() {
var a uint = 60 /* 60 = 0011 1100 */
var b uint = 13 /* 13 = 0000 1101 */
var c uint = 0
c = a & b /* 12 = 0000 1100 */
fmt.Printf("第一行 - c 的值为 %d\n", c) // 12
c = a | b /* 61 = 0011 1101 */
fmt.Printf("第二行 - c 的值为 %d\n", c) // 61
c = a ^ b /* 49 = 0011 0001 */
fmt.Printf("第三行 - c 的值为 %d\n", c) // 49
c = a << 2 /* 240 = 1111 0000 */
fmt.Printf("第四行 - c 的值为 %d\n", c) // 240
c = a >> 2 /* 15 = 0000 1111 */
fmt.Printf("第五行 - c 的值为 %d\n", c) // 15
}
赋值运算符
package main
import "fmt"
func main() {
var a int = 21
var c int
c = a
fmt.Printf("第 1 行 - = 运算符实例,c 值为 = %d\n", c) // 21
c += a
fmt.Printf("第 2 行 - += 运算符实例,c 值为 = %d\n", c) // 42
c -= a
fmt.Printf("第 3 行 - -= 运算符实例,c 值为 = %d\n", c) // 21
c *= a
fmt.Printf("第 4 行 - *= 运算符实例,c 值为 = %d\n", c) // 441
c /= a
fmt.Printf("第 5 行 - /= 运算符实例,c 值为 = %d\n", c) // 21
c = 200
c <<= 2
fmt.Printf("第 6 行 - <<= 运算符实例,c 值为 = %d\n", c) // 800
c >>= 2
fmt.Printf("第 7 行 - >>= 运算符实例,c 值为 = %d\n", c) // 200
c &= 2
fmt.Printf("第 8 行 - &= 运算符实例,c 值为 = %d\n", c) // 0
c ^= 2
fmt.Printf("第 9 行 - ^= 运算符实例,c 值为 = %d\n", c) // 2
c |= 2
fmt.Printf("第 10 行 - |= 运算符实例,c 值为 = %d\n", c) // 2
}
其他运算符
package main
import "fmt"
func main() {
var a int = 4
var b int32
var c float32
var ptr *int
/* 运算符实例 */
fmt.Printf("第 1 行 - a 变量类型为 = %T\n", a) // int
fmt.Printf("第 2 行 - b 变量类型为 = %T\n", b) // int32
fmt.Printf("第 3 行 - c 变量类型为 = %T\n", c) // float32
/* & 和 * 运算符实例 */
ptr = &a /* 'ptr' 包含了 'a' 变量的地址 */
fmt.Printf("a 的值为 %d\n", a) // 4
fmt.Printf("*ptr 为 %d\n", *ptr) // 4
}
运算符优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
优先级 | 运算符 |
---|---|
5 | * / % << >> & &^ |
4 | + - | ^ |
3 | == != < <= > >= |
2 | && |
1 | || |
可以通过使用括号来临时提升某个表达式的整体运算优先级
package main
import "fmt"
func main() {
var a int = 20
var b int = 10
var c int = 15
var d int = 5
var e int
e = (a + b) * c / d // ( 30 * 15 ) / 5
fmt.Printf("(a + b) * c / d 的值为 : %d\n", e) // 90
e = ((a + b) * c) / d // (30 * 15 ) / 5
fmt.Printf("((a + b) * c) / d 的值为 : %d\n", e) // 90
e = (a + b) * (c / d) // (30) * (15/5)
fmt.Printf("(a + b) * (c / d) 的值为 : %d\n", e) // 90
e = a + (b*c)/d // 20 + (150/5)
fmt.Printf("a + (b * c) / d 的值为 : %d\n", e) // 50
}
条件语句
Go 语言提供了以下几种条件判断语句:
语句 | 描述 |
---|---|
if 语句 | if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 |
if...else 语句 | if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 |
if 嵌套语句 | 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 |
switch 语句 | switch 语句用于基于不同条件执行不同动作。 |
select 语句 | select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 |
注意:Go 没有三目运算符,所以不支持 ?:
形式的条件判断。
switch 语句
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。
switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break。
switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
package main
import "fmt"
func main() {
/* 定义局部变量 */
var grade = "B"
var marks = 90
switch marks {
case 90:
grade = "A"
case 80:
grade = "B"
case 50, 60, 70:
grade = "C"
default:
grade = "D"
}
switch {
case grade == "A":
fmt.Printf("优秀!\n")
case grade == "B", grade == "C":
fmt.Printf("良好\n")
case grade == "D":
fmt.Printf("及格\n")
case grade == "F":
fmt.Printf("不及格\n")
default:
fmt.Printf("差\n")
}
fmt.Printf("你的等级是 %s\n", grade) // A
}
Type Switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
package main
import "fmt"
func main() {
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T", i) // x 的类型 :<nil>
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
}
fallthrough
使用 fallthrough
会强制执行后面的 case 语句,fallthrough
不会判断下一条 case 的表达式结果是否为 true
。
package main
import "fmt"
func main() {
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true") // true
fallthrough
case false:
fmt.Println("3、case 条件语句为 false") // true
fallthrough
case true:
fmt.Println("4、case 条件语句为 true") // true
fallthrough
case false:
fmt.Println("5、case 条件语句为 false") // true
fallthrough
case true:
fmt.Println("6、case 条件语句为 true") // true
fallthrough
default:
fmt.Println("7、默认 case") // true
}
}
select 语句
select
是 Go 中的一个控制结构,类似于用于通信的 switch
语句。每个 case
必须是一个通信操作,要么是发送要么是接收。
select
随机执行一个可运行的 case
。如果没有 case
可运行,它将阻塞,直到有 case
可运行。一个默认的子句应该总是可运行的。
select
语句的语法:
-
每个
case
都必须是一个通信 -
所有
channel
表达式都会被求值 -
所有被发送的表达式都会被求值
-
如果任意某个通信可以进行,它就执行,其他被忽略。
-
如果有多个
case
都可以运行,select
会随机公平地选出一个执行。其他不会执行。否则:
- 如果有
default
子句,则执行该语句。 - 如果没有
default
子句,select
将阻塞,直到某个通信可以运行;Go 不会重新对channel
或值进行求值。
- 如果有
循环语句
Go 语言提供了以下几种类型循环处理语句:
循环类型 | 描述 |
---|---|
for 循环 | 重复执行语句块 |
循环嵌套 | 在 for 循环中嵌套一个或多个 for 循环 |
循环控制语句
GO 语言支持以下几种循环控制语句:
控制语句 | 描述 |
---|---|
break 语句 | 经常用于中断当前 for 循环或跳出 switch 语句 |
continue 语句 | 跳过当前循环的剩余语句,然后继续进行下一轮循环。 |
goto 语句 | 将控制转移到被标记的语句。 |
无限循环
package main
import "fmt"
func main() {
for true {
fmt.Printf("这是无限循环。\n")
}
}
for 循环
Go 语言的 For 循环有 3 种形式
// 类似于 C 语言的 for
for init; condition; post { }
// 类似于 C 语言的 while
for condition { }
// 类似于 C 语言的 for(;;)
for { }
- init : 一般为赋值表达式,给控制变量赋初值;
- condition : 关系表达式或逻辑表达式,循环控制条件;
- post : 一般为赋值表达式,给控制变量增量或减量。
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
示例:计算 1 到 10 的数字之和
package main
import "fmt"
func main() {
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}
fmt.Println(sum)
}
示例:在 sum 小于 10 的时候计算 sum 自相加后的值
package main
import "fmt"
func main() {
sum := 1
for sum <= 10 {
sum += sum
}
fmt.Println(sum) // 16
// 这样写也可以,更像 While 语句形式
sum = 1
for sum <= 20 {
sum += sum
}
fmt.Println(sum) // 32
}
示例:无限循环
package main
import "fmt"
func main() {
sum := 0
for {
sum++ // 无限循环下去
}
fmt.Println(sum) // 无法输出
}
For-each range 循环
这种格式的循环可以对字符串、数组、切片等进行迭代输出元素。
package main
import "fmt"
func main() {
strings := []string{"google", "runoob"}
for i, s := range strings {
fmt.Println(i, s)
}
numbers := [6]int{1, 2, 3, 5}
for i, x := range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i, x)
}
}
函数
最少有一个 main()
函数。
函数声明告诉了编译器函数的名称,返回类型,参数。
Go 语言标准库提供了多种内置的函数。例如,len()
函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
函数定义
函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
func
:函数由func
开始声明function_name
:函数名称,参数列表和返回值类型构成了函数签名parameter list
:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、参数个数。参数是可选的,也就是说函数也可以不包含参数return_types
:返回类型,函数返回一列值。return_types
是该列值的数据类型。有些功能不需要返回值,这种情况下return_types
不是必须的- 函数体:函数定义的代码集合
函数调用
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a = 100
var b = 200
var ret int
/* 调用函数并返回最大值 */
ret = max(a, b)
fmt.Printf("最大值是 : %d\n", ret) // 200
}
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 定义局部变量 */
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
函数返回多个值
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b) // Runoob Google
}
函数参数
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
传递类型 | 描述 |
---|---|
值传递 | 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 |
引用传递 | 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 |
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
值传递
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a = 100
var b = 200
fmt.Printf("交换前 a 的值为 : %d\n", a) // 100
fmt.Printf("交换前 b 的值为 : %d\n", b) // 200
/* 通过调用函数来交换值 */
swap(a, b)
fmt.Printf("交换后 a 的值 : %d\n", a) // 100
fmt.Printf("交换后 b 的值 : %d\n", b) // 200
}
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
return temp
}
引用传递
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a = 100
var b = 200
fmt.Printf("交换前,a 的值 : %d\n", a) // 100
fmt.Printf("交换前,b 的值 : %d\n", b) // 200
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后,a 的值 : %d\n", a) // 200
fmt.Printf("交换后,b 的值 : %d\n", b) // 100
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
函数用法
函数用法 | 描述 |
---|---|
函数作为另外一个函数的实参 | 函数定义后可作为另外一个函数的实参数传入 |
闭包 | 闭包是匿名函数,可在动态编程中使用 |
方法 | 方法就是一个包含了接受者的函数 |
函数作为实参
Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。
package main
import (
"fmt"
"math"
)
func main() {
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9)) // 3
}
package main
import "fmt"
// 声明一个函数类型
type cb func(int) int
func main() {
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}
闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必声明
package main
import "fmt"
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println("1::", nextNumber()) // 1
fmt.Println("2::", nextNumber()) // 2
fmt.Println("3::", nextNumber()) // 3
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println("4::", nextNumber1()) // 1
fmt.Println("5::", nextNumber1()) // 2
}
方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
package main
import (
"fmt"
)
/*
Circle
定义结构体
*/
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
Go 没有面向对象,常见的 Java 、C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this
(在上面的例子里是 c Circle
)指针需要明确的声明出来
变量作用域
作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为 局部变量
- 函数外定义的变量称为 全局变量
- 函数定义中的变量称为 形式参数
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
全局变量
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。
形式参数
形式参数会作为函数的局部变量来使用。
初始化局部和全局变量
不同类型的局部和全局变量默认值为:
数据类型 | 初始化默认值 |
---|---|
int | 0 |
float32 | 0 |
pointer | nil |
数组
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。
数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。
声明数组
Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
var variable_name [SIZE] variable_type
以上为一维数组的定义方式。例如以下定义了数组 balance 长度为 10 类型为 float32
:
var balance [10] float32
初始化数组
以下演示了数组初始化:
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
我们也可以通过字面量在声明数组的同时快速初始化数组:
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
如果数组长度不确定,可以使用 ...
代替数组的长度或忽略,编译器会根据元素个数自行推断数组的长度:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}
如果设置了数组的长度,我们还可以通过指定下标来初始化元素:
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1: 2.0, 3: 7.0}
初始化数组中 {}
中的元素个数不能大于 []
中的数字。
balance[4] = 50.0
以上实例读取了第五个元素。数组元素可以通过索引(位置)来读取(或者修改)
更多内容
数组对 Go 语言来说是非常重要的,以下我们将介绍数组更多的内容:
内容 | 描述 |
---|---|
多维数组 | Go 语言支持多维数组,最简单的多维数组是二维数组 |
向函数传递数组 | 你可以向函数传递数组参数 |
多维数组
多维数组声明方式:
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
声明一个三维的整型数组:
var threedim [5][10][4]int
二维数组
二维数组是最简单的多维数组,二维数组本质上是由一维数组组成的。二维数组定义方式如下:
var arrayName [x][y] variable_type
variable_type
为 Go 语言的数据类型,arrayName
为数组名,二维数组可认为是一个表格,x
为行,y
为列
package main
import "fmt"
func main() {
// Step 1: 创建数组
values := [][]int{}
// Step 2: 使用 append() 函数向空的二维数组添加两行一维数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1)
values = append(values, row2)
// Step 3: 显示两行数据
fmt.Println("Row 1")
fmt.Println(values[0])
fmt.Println("Row 2")
fmt.Println(values[1])
// Step 4: 访问第一个元素
fmt.Println("第一个元素为:")
fmt.Println(values[0][0])
}
输出结果为:
Row 1
[1 2 3]
Row 2
[4 5 6]
第一个元素为:
1
初始化二维数组
多维数组可通过大括号来初始值。以下实例为一个 3 行 4 列的二维数组:
a := [3][4]int{
{0, 1, 2, 3}, /* 第一行索引为 0 */
{4, 5, 6, 7}, /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
注意:以上代码中倒数第二行的 }
必须要有逗号,因为最后一行的 }
不能单独一行,也可以写成这样:
a := [3][4]int{
{0, 1, 2, 3}, /* 第一行索引为 0 */
{4, 5, 6, 7}, /* 第二行索引为 1 */
{8, 9, 10, 11}} /* 第三行索引为 2 */
以下实例初始化一个 2 行 2 列 的二维数组:
package main
import "fmt"
func main() {
// 创建二维数组
sites := [2][2]string{}
// 向二维数组添加元素
sites[0][0] = "Google"
sites[0][1] = "Runoob"
sites[1][0] = "Taobao"
sites[1][1] = "Weibo"
// 显示结果
fmt.Println(sites) // [[Google Runoob] [Taobao Weibo]]
}
访问二维数组
二维数组通过指定坐标来访问。如数组中的行索引与列索引,例如访问二维数组 val
第三行的第四个元素:
val := a[2][3]
var value int = a[2][3]
二维数组可以使用循环嵌套来输出元素:
package main
import "fmt"
func main() {
/* 数组 - 5 行 2 列*/
var a = [5][2]int{{0, 0}, {1, 2}, {2, 4}, {3, 6}, {4, 8}}
/* 输出数组元素 */
for i := 0; i < 5; i++ {
for j := 0; j < 2; j++ {
fmt.Printf("a[%d][%d] = %d\n", i, j, a[i][j])
}
}
}
创建各个维度元素数量不一致的多维数组:
package main
import "fmt"
func main() {
// 创建空的二维数组
var animals [][]string
// 创建三一维数组,各数组长度不同
row1 := []string{"fish", "shark", "eel"}
row2 := []string{"bird"}
row3 := []string{"lizard", "salamander"}
// 使用 append() 函数将一维数组添加到二维数组中
animals = append(animals, row1)
animals = append(animals, row2)
animals = append(animals, row3)
// 循环输出
for i := range animals {
fmt.Printf("Row: %v\n", i)
fmt.Println(animals[i])
}
}
向函数传递数组
如果你想向函数传递数组参数,你需要在函数定义时,声明形参为数组,我们可以通过以下两种方式来声明:
方式一:形参设定数组大小:
void myFunction(param [10]int)
{
.
.
.
}
方式二:形参未设定数组大小:
void myFunction(param []int)
{
.
.
.
}
实例:函数接收整型数组参数,另一个参数指定了数组元素的个数,并返回平均值:
package main
import "fmt"
func main() {
/* 数组长度为 5 */
var balance = [5]int{1000, 2, 3, 17, 50}
var avg float32
/* 数组作为参数传递给函数 */
avg = getAverage(balance, 5)
/* 输出返回的平均值 */
fmt.Printf("平均值为: %f ", avg) // 214.399994
}
func getAverage(arr [5]int, size int) float32 {
var i, sum int
var avg float32
for i = 0; i < size; i++ {
sum += arr[i]
}
avg = float32(sum) / float32(size)
return avg
}
指针
Go 语言中使用指针可以更简单的执行一些任务
变量是一种使用方便的占位符,用于引用计算机内存地址
Go 语言的取地址符是 &
,放到一个变量前使用就会返回相应变量的内存地址
在指针类型前面加上 *
号(前缀)来获取指针所指向的内容
指针声明格式如下:
var var_name *var-type
var-type
为指针类型,var_name
为指针变量名,*
号用于指定变量是作为一个指针。以下是有效的指针声明:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
指针使用流程:
- 定义指针变量
- 为指针变量赋值
- 访问指针变量中指向地址的值
package main
import "fmt"
func main() {
var a int = 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a) // c00000a098
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip) // c00000a098
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip) // 20
}
Go 空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil
nil
指针也称为空指针
nil
在概念上和其它语言的 null、None、nil、NULL 一样,都指代零值或空值
一个指针变量通常缩写为 ptr
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr) // 0
fmt.Printf("ptr 的值为 : %d\n", *ptr) // 报错:panic: runtime error: invalid memory address or nil pointer dereference
}
空指针判断:
if(ptr != nil) /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */
更多内容
内容 | 描述 |
---|---|
Go 指针数组 | 可以定义一个指针数组来存储地址 |
Go 指向指针的指针 | Go 支持指向指针的指针 |
Go 向函数传递指针参数 | 通过引用或地址传参,在函数调用时可以改变其值 |
指针数组
有一种情况,我们可能需要保存数组,这样我们就需要使用到指针。
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{10, 100, 200}
var i int
var ptr [MAX]*int
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, *ptr[i])
}
}
指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址
指向指针的指针变量声明格式如下:
var ptr **int;
实例:
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a) // 3000
fmt.Printf("指针变量 *ptr = %d\n", *ptr) // 3000
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr) // 3000
}
指针作为函数参数
Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。
实例:向函数传递指针,并在函数调用后修改函数内的值
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
fmt.Printf("交换前 a 的值 : %d\n", a) // 100
fmt.Printf("交换前 b 的值 : %d\n", b) // 200
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后 a 的值 : %d\n", a) // 200
fmt.Printf("交换后 b 的值 : %d\n", b) // 100
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}
结构体
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
定义结构体
结构体定义需要使用 type
和 struct
语句。struct
语句定义一个新的数据类型,结构体中有一个或多个成员。type
语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
实例如下:
package main
import "fmt"
type Books struct {
title string
author string
subject string
bookId int
}
func main() {
books := Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407}
// 创建一个新的结构体
fmt.Println(books) // {Go 语言 www.runoob.com Go 语言教程 6495407}
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", bookId: 6495407}) // {Go 语言 www.runoob.com Go 语言教程 6495407}
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"}) // {Go 语言 www.runoob.com 0}
}
访问结构体成员
要访问结构体成员,需要使用点号 . 操作符,格式为:
结构体.成员名
结构体作为函数参数
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
printBook(Book1)
/* 打印 Book2 信息 */
printBook(Book2)
}
func printBook(book Books) {
fmt.Printf("Book title : %s\n", book.title)
fmt.Printf("Book author : %s\n", book.author)
fmt.Printf("Book subject : %s\n", book.subject)
fmt.Printf("Book book_id : %d\n", book.book_id)
}
结构体指针
定义指向结构体的指针类似于其他指针变量,格式如下:
var struct_pointer *Books
以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 &
符号放置于结构体变量前:
struct_pointer = &Book1
使用结构体指针访问结构体成员,使用 .
操作符:
struct_pointer.title
使用结构体指针重写以上实例,代码如下:
package main
import "fmt"
type Books struct {
title string
author string
subject string
bookId int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.bookId = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.bookId = 6495700
/* 打印 Book1 信息 */
printBook(&Book1)
/* 打印 Book2 信息 */
printBook(&Book2)
}
func printBook(book *Books) {
fmt.Printf("Book title : %s\n", book.title)
fmt.Printf("Book author : %s\n", book.author)
fmt.Printf("Book subject : %s\n", book.subject)
fmt.Printf("Book book_id : %d\n", book.bookId)
}
切片(Slice)
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片
你可以声明一个未指定大小的数组来定义切片:
var identifier []type
切片不需要说明长度。
或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len)
// 简写为
slice1 := make([]type, len)
也可以指定容量,其中 capacity
为可选参数。
make([]T, length, capacity)
这里 len
是数组的长度并且也是切片的初始长度。
切片初始化
s :=[] int {1,2,3 }
直接初始化切片,[]
表示是切片类型,{1,2,3}
初始化值依次是 1,2,3
,其中 cap=len=3
。
s := arr[:]
初始化切片 s
,是数组 arr
的引用。
s := arr[startIndex:endIndex]
将 arr
中从下标 startIndex
到 endIndex-1
下的元素创建为一个新的切片。
s := arr[startIndex:]
默认 endIndex
时将表示一直到 arr
的最后一个元素。
s := arr[:endIndex]
默认 startIndex
时将表示从 arr
的第一个元素开始。
s1 := s[startIndex:endIndex]
通过切片 s
初始化切片 s1
。
s :=make([]int,len,cap)
通过内置函数 make()
初始化切片 s
,[]int
标识为其元素类型为 int
的切片。
len()
和 cap()
函数
切片是可索引的,并且可以由 len()
方法获取长度。
切片提供了计算容量的方法 cap()
可以测量切片最长可以达到多少。
package main
import "fmt"
func main() {
var numbers = make([]int, 3, 5)
printSlice(numbers)
}
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x) // len=3 cap=5 slice=[0 0 0]
}
空( nil
)切片
一个切片在未初始化之前默认为 nil
,长度为 0
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound]
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
printSlice(numbers) // len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
/* 打印原始切片 */
fmt.Println("numbers ==", numbers) // [0 1 2 3 4 5 6 7 8]
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4]) // [1 2 3]
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3]) // [0 1 2]
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:]) // [4 5 6 7 8]
numbers1 := make([]int, 0, 5)
printSlice(numbers1) // len=0 cap=5 slice=[]
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2) // len=2 cap=9 slice=[0 1]
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3) // len=3 cap=7 slice=[2 3 4]
}
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
append()
和 copy()
函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers) // len=0 cap=0 slice=[]
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers) // len=1 cap=1 slice=[0]
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers) // len=2 cap=2 slice=[0 1]
/* 同时添加多个元素 */
numbers = append(numbers, 2, 3, 4)
printSlice(numbers) // len=5 cap=6 slice=[0 1 2 3 4]
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1, numbers)
printSlice(numbers1) // len=5 cap=12 slice=[0 1 2 3 4]
}
func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
范围(Range)
Go 语言中 range
关键字用于 for
循环中迭代数组(array)、切片(slice)、通道(channel) 或集合(map) 的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
package main
import "fmt"
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum) // 9
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i) // 1
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}
输出结果为:
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111
Map(集合)
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
定义 Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
如果不初始化 map,那么就会创建一个 nil
map。nil
map 不能用来存放键值对
下面实例演示了创建和使用 map
package main
import "fmt"
func main() {
var countryCapitalMap map[string]string /*创建map */
countryCapitalMap = make(map[string]string)
/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap["France"] = "巴黎"
countryCapitalMap["Italy"] = "罗马"
countryCapitalMap["Japan"] = "东京"
countryCapitalMap["India "] = "新德里"
/*使用键输出地图值 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap[country])
}
/*查看元素在集合中是否存在 */
capital, ok := countryCapitalMap["American"] /*如果确定是真实的,则存在,否则不存在 */
fmt.Println(capital) //
fmt.Println(ok) // false
if ok {
fmt.Println("American 的首都是", capital)
} else {
fmt.Println("American 的首都不存在") // true
}
}
delete()
函数
delete()
函数用于删除集合的元素, 参数为 map
和其对应的 key
delete(countryCapitalMap, "France")
递归函数
Go 语言支持递归。
实例:阶乘
package main
import "fmt"
func Factorial(n uint64) (result uint64) {
if n > 0 {
result = n * Factorial(n-1)
return result
}
return 1
}
func main() {
var i = 15
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}
实例:斐波那契数列
package main
import "fmt"
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-2) + fibonacci(n-1)
}
func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\t", fibonacci(i))
}
}
类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go 语言类型转换基本格式如下:
type_name(expression)
type_name
为类型,expression
为表达式。
以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:
package main
import "fmt"
func main() {
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum) / float32(count)
fmt.Printf("mean 的值为: %f\n", mean) // 3.400000
}
接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
实例:NokiaPhone
和 IPhone
实现了接口 Phone
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call() // I am Nokia, I can call you!
phone = new(IPhone)
phone.call() // I am iPhone, I can call you!
}
错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error
类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
我们可以在编码中通过实现 error
接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用 errors.New
可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
实例:
package main
import (
"fmt"
)
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
输出结果为:
100/10 = 10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0
并发
Go 语言支持并发,我们只需要通过 go
关键字来开启 goroutine
即可。
goroutine
是轻量级线程,goroutine
的调度是由 Golang 运行时进行管理的。
goroutine
语法格式:
go 函数名( 参数列表 )
Go 允许使用 go
语句开启一个新的运行期线程, 即 goroutine
,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine
共享同一个地址空间。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
执行以上代码,你会看到输出的 hello
和 world
是没有固定先后顺序。因为它们是两个 goroutine
在执行
通道(channel)
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine
之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
声明一个通道很简单,我们使用 chan
关键字即可,通道在使用前必须先创建:
ch := make(chan int)
注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
实例:通过两个 goroutine
来计算数字之和,在 goroutine
完成计算后,它会计算两个结果的和:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y) // -5 17 12
}
通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
package main
import "fmt"
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
遍历通道与关闭通道
Go 通过 range
关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok
就为 false
,这时通道就可以使用 close()
函数来关闭。
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}