go语言基础学习第一部分 (大地老师)
Go语言基础学习1
跟着B站大地老师go语言教程整理的笔记,基础学习第一部分
1. 基础
1. 变量定义
-
var 定义变量var 变量名类型= 表达式
var name string = "zhangsan" var name, age = "zhangsan", 20 //类型推导 var name = "Q1mi" var age = 18 //批量声明变量的时候指定类型 var ( a string b int c bool ) a = "张三" b = 10 c = true //批量声明变量并赋值 var ( a string = "张三" b int = 20 c bool = true )
-
类型推导方式定义变量
短变量只能用于声明局部变量,不能用于全局变量的声明
变量名:= 表达式
n := 10
_多用于占位,表示忽略值
-
常量
声明了pi 和e 这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。
const pi = 3.1415 const e = 2.7182 const ( pi = 3.1415 e = 2.7182 ) //常量n1、n2、n3 的值都是100。 const ( n1 = 100 n2 n3 )
2. fmt 包、Print、Println、Printf
-
一次输入多个值的时候Println 中间有空格Print 没有
-
Println 会自动换行,Print 不会
import "fmt"
func main(){
fmt.Println("go", "python", "php", "javascript") // go python php javascript
fmt.Print("go", "python", "php", "javascript") // gopythonphpjavascript
fmt.Println("hello")
fmt.Println("world")
// hello
// world
fmt.Print("hello")
fmt.Print("world")
// helloworld
}
- Printf 是格式化输出,在很多场景下比Println 更方便
a := 10
b := 20
c := 30
fmt.Println("a=", a, ",b=", b, ",c=", c) //a= 10 ,b= 20 ,c= 30
fmt.Printf("a=%d,b=%d,c=%d", a, b, c) //a=10,b=20,c=30
%d 是占位符,表示数字的十进制表示。Printf 中的占位符与后面的数字变量一一对应。
3. Go 语言中的注释
/*
这是一个注释
*/
//这是一个注释
4. 数据类型
4.1 整型
注意事项:实际项目中整数类型、切片、map 的元素数量等都可以用int 来表示。在涉及到二进制传输、为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int 和uint。
package main
import (
"fmt"
)
func main() {
var num int64
num = 123
fmt.Printf("值:%v 类型%T", num, num)
}
unsafe.Sizeof(n1) 是unsafe 包的一个函数,可以返回n1 变量占用的字节数
import (
"fmt"
"unsafe"
)
func main() {
var a int8 = 120
fmt.Printf("%T\n", a)
fmt.Println(unsafe.Sizeof(a))
}
int 不同长度直接的转换
import (
"fmt"
)
func main() {
var num1 int8
num1 = 127
num2 := int32(num1)
fmt.Printf("值:%v 类型%T", num2, num2) //值:127 类型int32
}
4.2 浮点型
Go 语言支持两种浮点型数:float32 和float64。这两种浮点型数据格式遵循IEEE 754 标准:float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32。float64 的浮点数的最大范围约为1.8e308,可以使用一个常量定义:math.MaxFloat64。
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) //默认保留6 位小数
fmt.Printf("%.2f\n", math.Pi) //保留2 位小数
}
Go 语言中浮点数默认是float64
num := 1.1
fmt.Printf("值:%v--类型:%T", num, num) //值:1.1--类型:float64
Golang 中float 精度丢失问题
d := 1129.6
fmt.Println((d * 100)) //输出:112959.99999999999
var d float64 = 1129.6
fmt.Println((d * 100)) //输出:112959.99999999999
m1 := 8.2
m2 := 3.8
fmt.Println(m1 - m2) // 期望是4.4,结果打印出了4.399999999999999
使用第三方包来解决精度损失问题:
https://github.com/shopspring/decimal
Golang 科学计数法表示浮点类型
num8 := 5.1234e2 // ? 5.1234 * 10 的2 次方
num9 := 5.1234E2 // ? 5.1234 * 10 的2 次方shift+alt+向下的箭头
num10 := 5.1234E-2 // ? 5.1234 / 10 的2 次方0.051234
fmt.Println("num8=", num8, "num9=", num9, "num10=", num10)
4.3 布尔值
Go 语言中以bool 类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。
注意:
- 布尔类型变量的默认值为false。
- Go 语言中不允许将整型强制转换为布尔型.
- 布尔型无法参与数值运算,也无法与其他类型进行转换。
import (
"fmt"
"unsafe"
)
func main() {
var b = true
fmt.Println(b, "占用字节:", unsafe.Sizeof(b))
}
4.4 字符串
Go 语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。Go 语言里的字符串的内部实现使用UTF-8 编码。字符串的值为双引号(")中的内容,可以在Go 语言的源码中直接添加非ASCII 码字符,例如:
s1 := "hello"
s2 := "你好"
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Code\\demo\\go.exe\"")
}
多行字符串
//Go 语言中要定义一个多行字符串时,就必须使用反引号字符:
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
- len(str)求字符串的长度
var str = "this is str"
fmt.Println(len(str))
- 拼接字符串
var str1 = "你好"
var str2 = "golang"
fmt.Println(str1 + str2)
var str3 = fmt.Sprintf("%v %v", str1, str2)
fmt.Println(str3)
//------------------------------------
var str = "this is golang"
var flag = strings.Contains(str, "golang")
fmt.Println(flag)
- strings.Split 分割字符串
var str = "123-456-789"
var arr = strings.Split(str, "-")
fmt.Println(arr)
- 判断首字符尾字母是否包含指定字符
var str = "this is golang"
var flag = strings.HasPrefix(str, "this")
fmt.Println(flag)
var str = "this is golang"
var flag = strings.HasSuffix(str, "go")
fmt.Println(flag)
判断字符串出现的位置
var str = "this is golang"
var index = strings.Index(str, "is") //从前往后
fmt.Println(index)
var str = "this is golang"
var index = strings.LastIndex(str, "is") //从后网前
fmt.Println(index)
Join 拼接字符串
var str = "123-456-789"
var arr = strings.Split(str, "-")
var str2 = strings.Join(arr, "*")
fmt.Println(str2)
4.5 byte 和rune 类型
组成每个字符串的元素叫做“字符”,可以通过遍历字符串元素获得字符。字符用单引号(’)包裹起来,如:
import "fmt"
func main() {
a := 'a'
b := '0'
//当我们直接输出byte(字符)的时候输出的是这个字符对应的码值
fmt.Println(a)
fmt.Println(b)
//如果我们要输出这个字符,需要格式化输出
fmt.Printf("%c--%c", a, b) //%c 相应Unicode 码点所表示的字符
}
字节(byte):是计算机中数据处理的基本单位,习惯上用大写B 来表示,1B(byte,字节)= 8bit(位)
字符:是指计算机中使用的字母、数字、字和符号
一个汉子占用3 个字节一个字母占用一个字节
a := "m"
fmt.Println(len(a)) //1
b := "张"
fmt.Println(len(b)) //3
Go 语言的字符有以下两种:
- uint8 类型,或者叫byte 型,代表了ASCII 码的一个字符。
- rune 类型,代表一个UTF-8 字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune 类型。rune 类型实际是一个int32。
Go 使用了特殊的rune 类型来处理Unicode,让基于Unicode 的文本处理更为方便,也可以使用byte 型进行默认字符串处理,性能和扩展性都有照顾。
//遍历字符串
import "fmt"
func main() {
s := "hello 张三"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}
输出:
104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 229(å) 188(¼) 160( ) 228(ä) 184(¸) 137() 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 24352(张) 19977(三)
因为UTF8 编码下一个中文汉字由3 个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。字符串底层是一个byte 数组,所以可以和[]byte 类型相互转换。字符串是不能修改的字符串是由byte 字节组成,所以字符串的长度是byte 字节的长度。rune 类型用来表示utf8 字符,一个rune 字符由一个或多个byte 组成。
rune 类型实际是一个int32
c3 := "营"
c4 := '营'
fmt.Printf("C3 的类型%T--C4 的类型%T", c3, c4) //C3 的类型string--C4 的类型int32c3 := "营"
c4 := '营'
fmt.Printf("C3 的类型%T--C4 的类型%T", c3, c4) //C3 的类型string--C4 的类型int32
4.6 修改字符串
要修改字符串,需要先将其转换成[]rune 或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
5. 基本数据类型之间的转换
5.1 数值类型之间的相互转换
import "fmt"
func main() {
var a int8 = 20
var b int16 = 40
var c = int16(a) + b //要转换成相同类型才能运行
fmt.Printf("值:%v--类型%T", c, c) //值:60--类型int16
}
import "fmt"
func main() {
var a float32 = 3.2
var b int16 = 6
var c = a + float32(b)
fmt.Printf("值:%v--类型%T", c, c) //值:9.2--类型float32
}
转换的时候建议从低位转换成高位,高位转换成低位的时候如果转换不成功就会溢出,和我们想的结果不一样。
func main() {
var a int16 = 129
var b = int8(a) // 范围-128 到127
println("b=", b) //b= -127 //错误
}
比如计算直角三角形的斜边长时使用math 包的Sqrt()函数,该函数接收的是float64 类型的参数,而变量a 和b 都是int 类型的,这个时候就需要将a 和b 强制类型转换为float64 类型。
var a, b = 3, 4
var c int
// math.Sqrt()接收的参数是float64 类型,需要强制转换
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)
5.2 其他类型转换成String 类型
- sprintf 把其他类型转换成string 类型
import "fmt"
func main() {
var i int = 20
var f float64 = 12.456
var t bool = true
var b byte = 'a'
var strs string
strs = fmt.Sprintf("%d", i)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
strs = fmt.Sprintf("%f", f)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
strs = fmt.Sprintf("%t", t)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
strs = fmt.Sprintf("%c", b)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
}
输出:
str type string ,strs=20
str type string ,strs=12.456000
str type string ,strs=true
str type string ,strs=a
- 使用strconv 包里面的几种转换方法进行转换
import (
"fmt"
"strconv"
)
func main() {
//1、int 转换成string
var num1 int = 20
s1 := strconv.Itoa(num1)
fmt.Printf("str type %T ,strs=%v \n", s1, s1)
// 2、float 转string
var num2 float64 = 20.113123
/*
参数1:要转换的值
参数2:格式化类型
'f'(-ddd.dddd)、
'b'(-ddddp±ddd,指数为二进制)、
'e'(-d.dddde±dd,十进制指数)、
'E'(-d.ddddE±dd,十进制指数)、
'g'(指数很大时用'e'格式,否则'f'格式)、
'G'(指数很大时用'E'格式,否则'f'格式)。
参数3: 保留的小数点-1(不对小数点格式化)
参数4:格式化的类型
*/
s2 := strconv.FormatFloat(num2, 'f', 2, 64)
fmt.Printf("str type %T ,strs=%v \n", s2, s2)
// 3、bool 转string
s3 := strconv.FormatBool(true)
fmt.Printf("str type %T ,strs=%v \n", s3, s3)
//4、int64 转string
var num3 int64 = 20
/*
第二个参数为进制
*/
s4 := strconv.FormatInt(num3, 10)
fmt.Printf("类型%T ,strs=%v \n", s4, s4)
}
5.3 String 类型转换成数值类型
- string 类型转换成int 类型
var s = "1234"
i64, _ := strconv.ParseInt(s, 10, 64)
fmt.Printf("值:%v 类型:%T", i64, i64)
- string 类型转换成float 类型
str := "3.1415926535"
v1, _ := strconv.ParseFloat(str, 32)
v2, _ := strconv.ParseFloat(str, 64)
fmt.Printf("值:%v 类型:%T\n", v1, v1)
fmt.Printf("值:%v 类型:%T", v2, v2)
- string 类型转换成bool 类型(意义不大)
b, _ := strconv.ParseBool("true") // string 转bool
fmt.Printf("值:%v 类型:%T", b, b)
- string 转字符
s := "hello 张三"
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
注意:在go 语言中数值类型没法直接转换成bool 类型bool 类型也没法直接转换成数值类型
6. 流程
6.1 if else
-
基本写法
func ifDemo1() { score := 65 if score >= 90 { fmt.Println("A") } else if score > 75 { fmt.Println("B") } else { fmt.Println("C") } }
-
特殊写法
if 条件判断还有一种特殊的写法,可以在if 表达式之前添加一个执行语句,再根据变量值进行判断。
if score := 56; score >= 90 { fmt.Println("A") } else if score > 75 { fmt.Println("B") } else { fmt.Println("C") }
6.2 for循环
Go 语言中的所有循环类型均可以使用for 关键字来完成。
for 循环的基本格式如下:
for 初始语句;条件表达式;结束语句{
循环体语句
}
条件表达式返回true 时循环体不停地进行循环,直到条件表达式返回false 时自动退出循环。
for i := 0; i < 10; i++ {
fmt.Println(i)
}
for 循环的初始语句可以被忽略,但是初始语句后的分号必须要写
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
for 循环的初始语句和结束语句都可以省略
i := 0
for i < 10 {
fmt.Println(i)
i++
}
这种写法类似于其他编程语言中的while,在while 后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
注意:Go 语言中是没有while 语句的,我们可以通过for 代替
无限循环:
k := 1
for { // 这里也等价for ; ; {
if k <= 10 {
fmt.Println("ok~~", k)
} else {
break //break 就是跳出这个for 循环
}
k++
}
for range(键值循环)
Go 语言中可以使用for range 遍历数组、切片、字符串、map 及通道(channel)。通过for range 遍历的返回值有以下规律:
- 数组、切片、字符串返回索引和值。
- map 返回键和值。
- 通道(channel)只返回通道内的值。
str := "abc 上海"
for index, val := range str {
fmt.Printf("index=%d, val=%c \n", index, val)
}
str := "abc 上海"
for _, val := range str {
fmt.Printf("val=%c \n", val)
}
6.3 switch case
extname := ".a"
switch extname {
case ".html":
fmt.Println("text/html")
break
case ".css":
fmt.Println("text/css")
break
case ".js":
fmt.Println("text/javascript")
break
default:
fmt.Println("格式错误")
break
}
Go 语言中每个case 语句中可以不写break,不加break 也不会出现穿透的现象
extname := ".a"
switch extname {
case ".html":
fmt.Println("text/html")
case ".css":
fmt.Println("text/css")
case ".js":
fmt.Println("text/javascript")
default:
fmt.Println("格式错误")
}
一个分支可以有多个值,多个case 值中间使用英文逗号分隔。
n := 2
switch n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
分支还可以使用表达式,这时候switch 语句后面不需要再跟判断变量。
age := 56
switch {
case age < 25:
fmt.Println("好好学习吧!")
case age > 25 && age <= 60:
fmt.Println("好好工作吧!")
case age > 60:
fmt.Println("好好享受吧!")
default:
fmt.Println("活着真好!")
}
switch 的穿透fallthrought
fallthrough`语法可以执行满足条件的case 的下一个case,是为了兼容C 语言中的case 设计的。
func switchDemo5() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}
输出:
a
b
var num int = 10
switch num {
case 10:
fmt.Println("ok1")
fallthrough //默认只能穿透一层
case 20:
fmt.Println("ok2")
fallthrough
case 30:
fmt.Println("ok3")
default:
fmt.Println("没有匹配到..")
}
输出:
ok1
ok2
ok3
6.4 break(跳出循环)
Go 语言中break 语句用于以下几个方面:
-
用于循环语句中跳出循环,并开始执行循环之后的语句。
-
break 在switch(开关语句)中在执行一条case 后跳出语句的作用。
-
在多重循环中,可以用标号label 标出想break 的循环。
-
switch(开关语句)中在执行一条case 后跳出语句的作用。
-
for 循环中默认break 只能跳出一层循环
-
在多重循环中,可以用标号label 标出想break 的循环。
import "fmt"
func main() {
lable2:
for i := 0; i < 2; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break lable2
}
fmt.Println("i j 的值", i, "-", j)
}
}
}
6.5 continue(继续下次循环)
continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for 循环内使用。
import "fmt"
func main() {
for i := 0; i < 2; i++ {
for j := 0; j < 4; j++ {
if j == 2 {
continue
}
fmt.Println("i j 的值", i, "-", j)
}
}
}
输出:
i j 的值0 - 0
i j 的值0 - 1
i j 的值0 - 3
i j 的值1 - 0
i j 的值1 - 1
i j 的值1 - 3
在continue 语句后添加标签时,表示开始标签对应的循环。
import "fmt"
func main() {
here:
for i := 0; i < 2; i++ {
for j := 0; j < 4; j++ {
if j == 2 {
continue here
}
fmt.Println("i j 的值", i, "-", j)
}
}
}
输出:
i j 的值0 - 0
i j 的值0 - 1
i j 的值1 - 0
i j 的值1 - 1
6.6 goto(跳转到指定标签)
goto 语句通过标签进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go 语言中使用goto 语句能简化一些代码的实现过程。
import "fmt"
func main() {
var n int = 30
fmt.Println("ok1")
if n > 20 {
goto label1
}
fmt.Println("ok2")
fmt.Println("ok3")
fmt.Println("ok4")
label1:
fmt.Println("ok5")
fmt.Println("ok6")
fmt.Println("ok7")
}
输出:
ok1
ok5
ok6
ok7
使用goto 语句能简化代码:
import "fmt"
func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for 循环")
}
输出:
0-0
0-1
2. 高级
1. 数组
数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),这种类型可以是任意的原始类型,比如int、string 等,也可以是用户自定义的类型。一个数组包含的元素个数被称为数组的长度。在Golang 中数组是一个长度固定的数据类型,数组的长度是类型的一部分,也就是说[5]int 和[10]int 是两个不同的类型。Golang中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地址中的,因而索引数组元素的速度非常快。
和数组对应的类型是Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活,但是想要理解slice 工作原理的话需要先理解数组。
// 定义一个长度为3 元素类型为int 的数组a
var a [3]int
// 定义一个长度为3 元素类型为int 的数组b 并赋值
var b [3]int
b[0] = 80
b[1] = 100
b[2] = 96
1.1 数组定义
var 数组变量名[元素数量]T
比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。[5]int 和[4]int 是不同的类型。
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时a 和b 是不同的类型
数组可以通过下标进行访问,下标是从0 开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。
1.2 数组的初始化
- 初始化数组时可以使用初始化列表来设置数组元素的值。
func main() {
var testArray [3]int //数组会初始化为int 类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2 0]
fmt.Println(cityArray) //[北京上海深圳]
}
- 按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度。
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京上海深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}
- 我们还可以使用指定索引值的方式来初始化数组。
func main() {
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}
1.3 数组的遍历
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for 循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range 遍历
for index, value := range a {
fmt.Println(index, value)
}
}
1.4 数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
func modifyArray(x [3]int) {
x[0] = 100
}
func modifyArray2(x [3][2]int) {
x[2][0] = 100
}
func main() {
a := [3]int{10, 20, 30}
modifyArray(a) //在modify 中修改的是a 的副本x
fmt.Println(a) //[10 20 30]
b := [3][2]int{
{1, 1},
{1, 1},
{1, 1},
}
modifyArray2(b) //在modify 中修改的是b 的副本x
fmt.Println(b) //[[1 1] [1 1] [1 1]]
}
注意:
- 数组支持“==“、”!=” 操作符,因为内存总是被初始化过的。
- [n]*T 表示指针数组,*[n]T 表示数组指针
2. 切片
2.1 切片的定义
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性
func arraySum(x [4]int) int {
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
func main() {
a := [4]int{1, 2, 3, 4}
println(arraySum(a))
b := [5]int{1, 2, 3, 4, 5}
println(arraySum(b)) //错误
}
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。声明切片类型的基本语法如下:
var name []T
其中:
- name:表示变量名
- T:表示切片中的元素类型
import "fmt"
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil 比较
}
当你声明了一个变量, 但却还并没有赋值时, golang 中会自动给你的变量赋值一个默认零值。这是每种类型对应的零值。
bool -> false
numbers -> 0
string-> ""
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil
2.2 切片的循环遍历
var a = []string{"北京", "上海", "深圳"}
// 方法1:for 循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range 遍历
for index, value := range a {
fmt.Println(index, value)
}
2.3 基于数组定义切片
func main() {
// 基于数组定义切片
a := [5]int{55, 56, 57, 58, 59}
b := a[1:4] //基于数组a 创建切片,包括元素a[1],a[2],a[3]
fmt.Println(b) //[56 57 58]
fmt.Printf("type of b:%T\n", b) //type of b:[]int
}
还支持如下方式:
c := a[1:] //[56 57 58 59]
d := a[:4] //[55 56 57 58]
e := a[:] //[55 56 57 58 59]
2.4 切片再切片
func main() {
//切片再切片
a := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}
fmt.Printf("a:%v type:%T len:%d cap:%d\n", a, a, len(a), cap(a))
b := a[1:3]
fmt.Printf("b:%v type:%T len:%d cap:%d\n", b, b, len(b), cap(b))
c := b[1:5]
fmt.Printf("c:%v type:%T len:%d cap:%d\n", c, c, len(c), cap(c))
}
输出:
a:[北京上海广州深圳成都重庆] type:[6]string len:6 cap:6
b:[上海广州] type:[]string len:2 cap:5
c:[广州深圳成都重庆] type:[]string len:4 cap:4
注意: 对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。
2.5 关于切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片s 的长度和容量可通过表达式len(s) 和cap(s) 来获取。
s := []int{2, 3, 5, 7, 11, 13}
fmt.Println(s)
fmt.Printf("长度:%v 容量%v\n", len(s), cap(s))
c := s[:2]
fmt.Println(c)
fmt.Printf("长度:%v 容量%v\n", len(c), cap(c))
d := s[1:3]
fmt.Println(d)
fmt.Printf("长度:%v 容量%v", len(d), cap(d))
输出:
[2 3 5 7 11 13]
长度:6 容量6
[2 3]
长度:2 容量6
[3 5]
长度:2 容量5
2.6 使用make()函数构造切片
我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数
make([]T, size, cap)
其中:
3. T:切片的元素类型
4. size:切片中元素的数量
5. cap:切片的容量
func main() {
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
}
上面代码中a 的内部存储空间已经分配了10 个,但实际上只用了2 个。容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。
2.7 切片不能直接比较
切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和nil 比较。一个nil 值的切片并没有底层数组,一个nil 值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0 的切片一定是nil
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
所以要判断一个切片是否是空的,要是用len(s) == 0 来判断,不应该使用s == nil 来判断。
2.8 切片是引用数据类型--注意切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1 直接赋值给s2,s1 和s2 共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}
2.9 append()方法为切片添加元素
Go 语言的内建函数append()可以为切片动态添加元素,每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append 函数的返回值。
给切片追加元素的错误写法:
s3 := []int{1, 2, 3, 5, 6, 7}
s3[6] = 8
fmt.Println(s3) //index out of range [6] with length 6
append()方法为切片追加元素:
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}
输出:
[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
从上面的结果可以看出:
- append()函数将元素追加到切片的最后并返回该切片。
- 切片numSlice 的容量按照1,2,4,8,16 这样的规则自动进行扩容,每次扩容后都是扩容前的2 倍。
append()函数还支持一次性追加多个元素。
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京上海广州深圳成都重庆]
切片的追加切片
s1 := []int{100, 200, 300}
s2 := []int{400, 500, 600}
s3 := append(s1, s2...)
fmt.Println(s3)
2.10 使用copy()函数复制切片
由于切片是引用类型,所以a 和b 其实都指向了同一块内存地址。修改b 的同时a 的值也会发生变化。
Go 语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
copy(destSlice, srcSlice []T)
其中:
- srcSlice: 数据来源切片
- destSlice: 目标切片
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用copy()函数将切片a 中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}
2.11 从切片中删除元素
Go 语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2 的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
总结一下就是:要从切片a 中删除索引为index 的元素,操作方法是a = append(a[:index], a[index+1:]...)
2.12 sort 排序
对于int 、float64 和string 数组或是切片的排序, go 分别提供了sort.Ints() 、sort.Float64s() 和sort.Strings() 函数, 默认都是从小到大排序。
intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
float8List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9, 31.4, 27.81828, 3.14}
stringList := []string{"a", "c", "b", "z", "x", "w", "y", "d", "f", "i"}
sort.Ints(intList)
sort.Float64s(float8List)
sort.Strings(stringList)
输出:
[0 1 2 3 4 5 6 7 8 9]
[3.14 4.2 5.9 10.2 12.4 27.81828 31.4 50.7 99.9]
[a b c d f i w x y z]
Golang 的sort 包可以使用sort.Reverse(slice) 来调换slice.Interface.Less ,也就是比较函数,所以, int 、float64 和string的逆序排序函数可以这么写。
intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
float8List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9, 31.4, 27.81828, 3.14}
stringList := []string{"a", "c", "b", "z", "x", "w", "y", "d", "f", "i"}
sort.Sort(sort.Reverse(sort.IntSlice(intList)))
sort.Sort(sort.Reverse(sort.Float64Slice(float8List)))
sort.Sort(sort.Reverse(sort.StringSlice(stringList)))
fmt.Printf("%v\n%v\n%v\n", intList, float8List, stringList)
输出:
[9 8 7 6 5 4 3 2 1 0]
[99.9 50.7 31.4 27.81828 12.4 10.2 5.9 4.2 3.14]
[z y x w i f d c b a]
3. map
3.1 map 定义
map 是一种无序的基于key-value 的数据结构,Go 语言中的map 是引用类型,必须初始化才能使用。Go 语言中map 的定义语法如下:
map[KeyType]ValueType
其中:
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map 类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:make: 用于slice,map,和channel 的初始化
make(map[KeyType]ValueType, [cap])
其中cap 表示map 的容量,该参数虽然不是必须的。
注意:获取map 的容量不能使用cap, cap 返回的是数组切片分配的空间大小, 根本不能用于map。要获取map 的容量,可以用len 函数。
3.2 map 基本使用
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
输出:
map[小明:100 张三:90]
100
type of a:map[string]int
map 也支持在声明的时候填充元素
func main() {
userInfo := map[string]string{
"username": "IT 营小王子",
"password": "123456",
}
fmt.Println(userInfo) //
}
3.3 判断某个键是否存在
Go 语言中有个判断map 中键是否存在的特殊写法
value, ok := map 对象[key]
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key 存在ok 为true,v 为对应的值;不存在ok 为false,v 为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
3.4 map 的遍历
Go 语言中使用for range 遍历map。
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
但我们只想遍历key 的时候,可以按下面的写法:
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}
注意: 遍历map 时的元素顺序与添加键值对的顺序无关。
3.5 使用delete()函数删除键值对
使用delete()内建函数从map 中删除一组键值对,delete()函数的格式如下:delete(map 对象, key)
其中,
- map 对象:表示要删除键值对的map 对象
- key:表示要删除的键值对的键
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
delete(scoreMap, "小明")//将小明:100 从map 中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}
3.6 元素为map 类型的切片
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map 元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "海淀区"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
3.7 值为切片类型的map
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
4. 函数
4.1 函数定义
func 函数名(参数)(返回值){
函数体
}
func intSum(x int, y int) int {
return x + y
}
func sayHello() {
fmt.Println("Hello it 营")
}
func main() {
sayHello()
ret := intSum(10, 20)
fmt.Println(ret)
}
4.2 函数参数
类型简写
func intSum(x, y int) int {
return x + y
}
可变参数
func intSum2(x ...int) int {
fmt.Println(x) //x 是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
调用上面的函数:
ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
func intSum3(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}
调用上述函数:
ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
4.3 返回值
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return 关键字返回。
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
4.4 函数类型与变量
定义函数类型
我们可以使用type 关键字来定义一个函数类型
type calculation func(int, int) int
上面语句定义了一个calculation 类型,它是一种函数类型,这种函数接收两个int 类型的参数并且返回一个int 类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation 类型的函数,例如下面的add 和sub 是calculation 类型。
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
add 和sub 都能赋值给calculation 类型的变量。
var c calculation
c = add
我们可以声明函数类型的变量并且为该变量赋值:
func main() {
var c calculation // 声明一个calculation 类型的变量c
c = add // 把add 赋值给c
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用add 一样调用c
f := add // 将函数add 赋值给变量f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add 一样调用f
}
4.5 高阶函数
高阶函数分为函数作为参数和函数作为返回值两部分。
函数可以作为参数:
func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
函数作为返回值:
import (
"fmt"
)
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func do(s string) func(int, int) int {
switch s {
case "+":
return add
case "-":
return sub
default:
return nil
}
}
func main() {
var a = do("+")
fmt.Println(a(10, 20))
}
4.6 匿名函数和闭包
匿名函数
func(参数)(返回值){
函数体
}
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
匿名函数多用于实现回调函数和闭包。
闭包
闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}
闭包进阶:
func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}
4.7 defer 语句
Go 语言中的defer 语句会将其后面跟随的语句进行延迟处理。在defer 归属的函数即将返回时,将延迟处理的语句按defer 定义的逆序进行执行,也就是说,先被defer 的语句最后被执行,最后被defer 的语句,最先被执行。
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出:
start
end
3
2
1
由于defer 语句延迟调用的特性,所以defer 语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
4.8 内置函数panic/recover
Go 语言中目前(Go1.12)是没有异常机制,但是使用panic/recover 模式来处理错误。panic可以在任何地方引发,但recover 只有在defer 调用的函数中有效。
- panic/recover 的基本使用
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
输出:
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
.../code/func/main.go:12
main.main()
.../code/func/main.go:20 +0x98
程序运行期间funcB 中引发了panic 导致程序崩溃,异常退出了。这个时候我们就可以通过recover 将程序恢复回来,继续往后执行。
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic 错误,可以通过recover 恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
注意:
- recover()必须搭配defer 使用。
- defer 一定要在可能引发panic 的语句之前定义。
defer 、recover 实现异常处理
func fn2() {
defer func() {
err := recover()
if err != nil {
fmt.Println("抛出异常给管理员发送邮件")
fmt.Println(err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}
defer 、panic、recover 抛出异常
func readFile(fileName string) error {
if fileName == "main.go" {
return nil
}
return errors.New("读取文件错误")
}
func fn3() {
defer func() {
err := recover()
if err != nil {
fmt.Println("抛出异常给管理员发送邮件")
}
}()
var err = readFile("xxx.go")
if err != nil {
panic(err)
}
fmt.Println("继续执行")
}
func main() {
fn3()
}