在经过3个月的Go的学习中 整理出来的比较 感觉还拿得出手。
文档资料:http://yuancao.meicx.com/d/34
书签网:https://www.bookstack.cn/
格式化:
gofmt -w hello.go
编译*指令:
go build -o 123.exe main.go
go build xxx.go
输入输出fmt:
var name string
fmt.Println("请输入姓名") //输出
fmt.Scanln(&name) //输入
Sprintf: 接受打印后的字符串赋给一个变量存储
dataStr := fmt.Sprintf("当前年月日 %d-%d-%d %d:%d:%d \n",time.Now().Year(),.....)
fmt.Println("dataStr=%v",dataStr)
进制*:
(1) 其他进制转十进制:
a. 二进制转十进制:
:: 从最低位开始(右边),将每个位上的数据提取出来,乘以2的(位数-1)次方,然后求和
1011 => 1*2零次方 + 1*2一次方 + 0*2二次方 + 1*2三次方 => 1+2+0+8 = 11
b. 八进制转十进制:
:: 从最低位开始(右边),将每个位上的数据提取出来,乘以8的(位数-1)次方,然后求和
0123 => 3*8零次方 + 2*8一次方 + 1*8二次方 + 0*8三次方 => 3+16+64+0 = 83
c. 十六进制转十进制:
:: 从最低位开始(右边),将每个位上的数据提取出来,乘以16的(位数-1)次方,然后求和
0x34A => 10*16零次方 + 4*16一次方 + 3*16二 次方 => 10 + 4*16 + 3*16*16 = 842
(2) 十进制转其他进制:
a. 十进制转二进制:
:: 将该数不断除以2,直到商为0为止,然后将每部得到的余数倒过来,就是对应的二进制
56 => 56/2余0 28/2余0 14/2余0 7/2余1 3/2余1 余1 => 111000
b. 十进制转八进制 (八进制以0开头)
:: 将该数不断除以8,直到商为0为止,然后将每部得到的余数倒过来,就是对应的八进制
156 => 156/8余
c. 十进制转十六进制: (十六进制以0x开头)
:: 将该数不断除以16,直到商为0为止,然后将每部得到的余数倒过来,就是对应的十六进制
356 => 356/16余4 22/16余6 余1 => 0x164
(3) 二进制转八进制和十六进制:
a. 二进制转八进制:
:: 将二进制数每三位一组(从低位开始组合),转换成对应的八进制数即可 0开头倒转
11010101 => 101转8#5 010转8#2 11转8#3 =>0325
b. 二进制转十六进制:
:: 将二进制数每四位一组(从低位开始组合),转换成对应的十六进制数即可 0x开头倒转
11010101 => 0101转16#5 1101转16#13(D) => 0xD5
(4) 八进制和十六进制转二进制:
a. 八进制转二进制:
:: 将八进制的每1位,转为对应的一个3位的二进制即可 倒转
0237 => 7转2#111 3转2#011 2转2#10 => 10011111
b. 十六进制转二进制:
:: 将十六进制的每1位,转为对应的一个4位的二进制即可 倒转
0x237 => 7转2#0111 3转2# 0011 2转2#10 => 1000110111
位运算*:
(1) 二进制的三个重要概念:原码,反码,补码
a. 对于有符号的而言:
1) 二进制的最高位是符号位: 0 表示正数, 1 表示负数
1 ==> [0000 0001] -1 ==> [1000 0001]
2) 正数的原码,反码,补码都一样
3) 负数的反码 = 它的源码符号位不变,其他位取反(0->1,1->0)
1 ==> 原码[0000 0001] 反码[0000 0001] 补码[0000 0001]
-1 ==> 原码[1000 0001] 反码[1111 1110] 补码[1111 1111]
4) 负数的补码 = 它的反码+1
5) 0的反码,补码都是0
6) *在计算机运算的时候,都是以补码的方式来运算
1-1 => 1 + (-1)
(2) go中有三个位运算 按位与(&),按位或(|),按位异或(^)
a. 按位与& : 两位全部为1,结果为1,否则为0
2&3 ==> 2的补码[0000 0010] 3的补码[0000 0011] 2&3 => [0000 0010] ->2
b. 按位或| : 两位只要有一个为1,结果为1,否则为0
2|3 ==> 2的补码[0000 0010] 3的补码[0000 0011] 2|3 => [0000 0011] ->3
c. 按位异或^ : 两位相同取0,不相同取1
2^3 ==> 2的补码[0000 0010] 3的补码[0000 0011] 2^3 => [0000 0001] ->1
-2^2 ==> -2的补码->原码[1000 0010]->反码[1111 1101]->补码[1111 1110]
2的补码[0000 0010]
-2^2 => [1111 1100](补码)->反码[1111 1011](补码-1)->原码[1000 0100](符号位不变其他取反) ->-4
(3) go中的2个移位运算符
a. >> 右移运算符 :: 低位溢出,符号位不变,并用符号位补溢出的高位
1>>2 ==> 1的补码[0000 0001] =>[0000 0000] = 0
b. << 左移运算符 :: 符号位不变,低位补0
1<<2 ==> 1补码[0000 0001] => [0000 0100] = 4
变量*的使用方法:
(1) 变量作用域:
a. 函数内申明/定义的变量交局部变量,作用域仅限于函数内部
b. 函数外部申明/定义的变量交全局变量,作用域在整个包都使用,如果其首字母大写,则作用域在整个程序都有效
c. 如果变量是在一个代码块,比如for/if中,那么该变量的作用域就是在该代码块中
(2) 变量的使用方法:
var name = "tom"
Name := "jack" //会报错,因为函数外部不能做赋值操作
func main(){
//golang 变量使用
//1. 指定变量类型 声明后不赋值 使用默认值
var i int
fmt.Println("i=",i)
//2. 根据数值类型判断(类型推导)
var num = 10.22
fmt.Println("num=",num)
//3. 省略var 使用 := 左侧的变量不应该是已经申明过的 否则编译会导致错误
// 等价于 var name string name := "tom"
name := "tom"
fmt.Println("name=",name)
}
(3) 多变量申明:
func main(){
//方式1
var n1, n2, n3 int
fmt.Println("n1=",n1,"n2=",n2,"n3=",n3)
//方式2
var n1, name ,ne = 100,"tom",888
fmt.Println("n1=",n1,"name=",name,"ne=",ne)
}
(4) 判断类型:
func main(){
var num = 10
//fmt.Printf()可以做格式化输出
fmt.Printf("num 的类型 T%", num)
// 如何在程序查看某个变量的占用字节大小和数据类型(使用较多)
var n1 int64 = 10
// unsafe.Sizeof(n1) 是unsafe包的一个函数 可以返回n1变量占用的字节数
fmt.Printf("n1 的类型 T% n1占用的字节数是 d%",n1, unsafe.Sizeof(n1))
}
(5) 基本数据类型转换:
func main(){
// 将i => float
var i int32 = 100
var n1 float32 = float32(i)
// %T 查看类型
fmt.Printf("n1 type is %T\n",n1)
//被转换的是变量存储的数值 变量本身的数据类型是不会改变的
fmt.Printf("i type is %T\n",i)
//在转换中 如将 int64 转成int8【-128---127】编译时不会报错
//只是转换的结果是按溢出处理, 和我们希望的结果不一样
var num1 int64 = 999999
var num2 int8 = int8(num1)
fmt.Println("num2 =",num2)
}
常量*:
常量的介绍:
a. 常量必须在申明时候初始化
b. 常量不能修改
c. 常量只能修饰bool,数值类型(int,float系列),string类型
d. 常量没有硬性规定常量必须大写字母
e. 依旧通过首字母大小写来控制常量的访问范围
常量的注意事项:
a. 比较简洁的写法:
const (
a = 1
b = 2
)
fmt.Println(a,b)
b. 还有一种专业的写法
const (
a = iota // 0
b // 1
c // 2
)
字符串*:
(1) 改变字符串:
func modifystring(){
s := "我hello word"
s1 := []rune(s) //字符串强转为数组
s1[0] = 'a'
str := string(s1) //数组转为字符串
fmt.Println(str)
s := "hello";
c := []byte(s)
c[0] = 'c'
s2 := string(c)
fmt.Printf("%s\n",s2)
a := "hello"
ss := "c" + a[1:] 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n",ss)
}
(2) 字符串常用的系统函数
1. len() => func len(v type) int
example:
len("aaa北") //返回6 一个汉字占3个字节
2. []rune(str):字符串遍历,同时处理有中文的问题 res := []rune(str) //吧字符串转rune的切片、
example:
str := "hello北京"
r := []rune(str)
for i=0; i n:
fmt.Println("you are bigger")
case input < n:
fmt.Println("you are less")
}
if flag { break }
}
}
(3) For 循环:
a. 常规用法:
func Printa(n int){
for i := 0; i <=n; i++ {
for j := 0; j < i; j++ {
fmt.Printf("A") //Printf 格式化打印A (横排)
}
fmt.Println() //换行
}
}
b. For range 语句:
str := "hello world 中国"
for i, v := range str { //i=>key v=>value
fmt.Printf("index[%d] val[%c] len[%d]\n",i,v,len([]byte(v)))
}
c. For true语句(死循环):
for true { fmt.Println(i) }
for { fmt.Println(i) } // 条件true 可省略(等效于上面)
d. goto 和 label语句
func main(){
LABEL:
HERE:
for i := 0; i < 6; i++ {
if i ==2 {
continue LABEL // 当满足条件后 跳到循环外部的LABEL 位置 作用域是一个函数内
goto HERE // 当满足条件后 跳到循环外部的HERE 位置 作用域是一个函数内
}
}
}
包*:go的每一个文件都属于一个包,也就是说Go是以包的形式来管理文件和项目目录结构的
(1) 包的三大作用:
a. 区分相同名字的函数,变量等标识符
b. 当程序文件过多时,可以很好的管理项目
c. 控制函数 变量等访问范围,即作用域
打包基本语法: package 包名
引入包语法: import "包名"
(2) 包的注意事项:
a. 在给一个文件打包时 该包对应一个文件夹.比如utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母
b. 当一个文件要使用其他包的函数或变量时,需要引入对应的包
c. package在文件第一行,然后是import指令
d. 在inport包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入
e. 为了让其他包的文件,可以访问到本包的函数,则改函数的首字母必须大写,类似其他语言的Public,这样才能跨包访问
f. 在访问其他包函数时,其语法是: 包名.函数名. 比如:Utils.Cal()
g. 如果包名过长,Go支持给包取别名. 注意细节:取别名后,原来的包名就不能用了
h. *如果需要编译成一个可执行的文件,就需要将这个包声明为main,即package main. 这就是语法规范.如果写一个库文件,包名可以自定义
i. *项目目录结构在$GOPATH的src下. 编译需进入在main包的目录下,
编译*指令:比如:E:\code\Go> go build -o bin/my.exe go_code/day2/code/demo/main //在GOPATH目录下生成一个存编译文件的目录
(3) 调用外部包和函数
package main
import(
"fmt"
"go_code/chaptes/function1/utils" //引入包路径 (GOPATG的src下路径开始不包含src)
//ut "go_code/chaptes/function1/utils" //取别名
)
return := utils.Cal(n1,n2,operator) //调用外部函数 需导入包名和函数名(函数名需首字母大写->该函数可导出)
函数*:
(1) 函数注意事项和细节:
a. 函数的形参列表可以是多个,返回值列表页可以是多个
b. 形参列表和返回值列表的数据类型可以是值类型也可以是引用类型
c. 函数的命名遵循标识符命名规范,首字母不能是数字,首字母是大写的函数可以被本包文件和其它包文件使用,类似Public,
首字母小写只能被本包文件使用,类型private
d. 函数中的变量是局部的,函数外部生效
e. 基本数据类型和数组默认是值传递的,即进行值拷贝. 在函数内修改,不会影响到原来的值
f. 如何希望函数内的变量能改变函数外的变量,可以传递变量的地址&,函数内以指针的方式操作变量
g. Go函数不支持重载
(2) 函数在内存分布说明:
a. 在调用一个函数时,会给函数分配一个新的空间(栈区),编译器会自身处理让这个新的空间和其它的栈的空间区分
b. 在每个函数对应的栈中,数据空间是独立的,不会混淆
c. 当一个函数调用完毕(执行完毕)后,程序会销毁这个函数的栈空间
1) 函数也是一种数据类型 可以赋值给一个变量,改变量就是一个函数类型的变量,通过该变量可以对函数调用
func add(a,b int) int { return a+b }
func main(){
c := add // add函数赋值变量c
sum := c(100,200)
fmt.Println(sum)
}
2) 函数既是一种数据类型,因此在go中函数可以做为形参,并且调用:
基本语法: type 自定义数据类型 数据类型 //理解:相当于一个别名
案列: type myint int //这时myint等价于int来使用
案列: type mySum func(int,int)int //这时mySum等价于一个函数类型
type op_func func(int,int) int //自定义类型 2个参数 1个返回值[函数也是一种类型] 相当于一个别名
func add(a,b int) int { return a+b }
func operator(op op_func, a, b int) int { //op 变量名,类型是上面自定义的op_func 参数是上面定义的两个int型
return op(a,b) //上面 op_func 类型 接受的2个参数
}
func main(){
c := add //add 函数赋值给变量c
sum := operator(c,100,200) // c函数当参数引用
fmt.Println(sum)
}
3)*函数传参的方法: 值传递和引用传递
注意:a. 无论是值传递还是引用传递 传递给函数的都是变量的副本 不过,值传递是值得拷贝。引用传递是地址的拷贝。一般来说,地址拷贝更为高效,
因为数据量小,而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
b. *值类型: 基本数据类型 int系列,float系列,bool,string,数组和结构体struct
b. *引用类型:map,slice切片,管道chan,指针,interface,默认都是引用的方式传递
4) 函数返回值命名:
func cale(a,b int) (sum int, avg int) {
sum = a+b
avg = (a+b)/2
return
}
5) _标识符 , 用来忽略返回值
func main(){ sum,_ := cale(100,200) }
6) 支持可变参数:(可变参数要放在形参列表最后)
func add(a int, arg... int) int {} //1个或多个参数
//注意: 其中 arg是一个slice 我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数
7) defer* :
(1) 在函数中,程序员经常需要创建资源(比如数据库连接,文件句柄,锁等),为了在函数执行完毕后及时释放资源,Go的设计者提供了defer(延时机制)
(2) 当函数执行defer时,暂时不会执行,会将defer后面的语句压入到独立的栈(defer栈),当函数执行完毕后,再从栈按先入后出的方式出栈
a. 当函数返回时 执行defer语句. 因此,可以用来做资源清理
b. 多个defer语句 按先进后出的方式执行
c. defer语句中的变量,在defer声明时就决定了
example1:
func a(){
i := 0
defer fmt.Println(i) //当函数返回结果之后才执行(定义一个压入一个栈里[先进后出])
i= 10
fmt.Println(i)
}
(3)
8) 匿名函数*:
a. 如果某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用
func test(a,b int) int {
result := func(a1,b1 int) int { return a1+b1 }(a,b) //(a,b)体现了调用
return result
}
b. 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
a := func(n1 int, n2 int) int { return n1-n2 }
res := a(10,20)
c. 全局匿名函数的使用
var ( //申明
Fun1 = func (n1 int,n2 int) int { return n1*n2 }
)
res := Fun1(2,5) //调用全局匿名函数
9) init 函数
a. 如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程顺序是: 变量定义->init函数->main函数
b. init函数主要作用是完成一些初始化的工作
内置函数*:
(1) close:用来关闭channel
(2) len:用来求长度, 比如string,array,slice,map,channel
(3) new*: 用来分配内存,主要用来分配值类型,比如int,struct,返回的是指针
j := new(int)
fmt.Printf("j的类型%T, j的值=%v,j的地址%v,j这个指针指向的值=%v\n",j,j,&j,*j)
//j的类型*int, j的值=0xc000062090,j的地址0xc00008c018,j这个指针指向的值=0
*j = 100
fmt.Println(*j) //地址内的值 100
(4) make*: 用来分配内存,主要用来分配引用类型,比如chan,map,slice,返回slice本身
s2 := make([]int,5)
fmt.Println(s2) // [0 0 0 0 0]
(5) append: 用来追加元素到数组,slice中
var a []int // [5] --> 数组 [] -->切片slice
a = append(a,10,20,33)
a = append(a,a...) //a... 相当于 展开上面的数组[10,20,33]
fmt.Println(a)
(6) panic和recover: 用来做错误处理
func test(){
defer func(){ //defer return 结束后调用
err := recover(); //recover() 内置函数,可以捕获异常
if err != nil { //nil 表示没有错误
fmt.Println("err=",err)
fmt.Println("发送邮箱给[email protected]")
}
}() //申明匿名函数并调用
b := 0
a := 100/b
fmt.Println(a)
return
}
func main(){
for {
test()
time.Sleep(time.Second)
}
}
递归函数*: =一个函数在体内调用了本身,则称为递归调用
(1) 递归函数的重要原则:
a. 执行一个函数时,就会创建一个新的受保护的独立空间
b. 函数的局部变量是独立的,不会相互影响
c. 递归必须向退出递归的条件逼近,否则就是无限递归
func test(n int){
fmt.Println("hello word")
time.Sleep(time.Second)
if n>10 { //出口条件 跳出循环
return
}
test(n+1)
}
闭包*: 一个函数和与其相关的引用环境组合而成的实体
//累加器
//a.返回的是一个匿名函数,但是这个匿名函数引用到函数外的x,因此这个匿名函数和x形成一个整体,构成闭包
//b.可以这样理解:闭包是类,匿名函数是操作(方法),x是字段(属性)
//c.我们反复调用f函数时,因为a是初始化一次,故每次调用一次就进行累计
//d.我们要搞清楚闭包的关键,就是分析出返回函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包
func Adder() func(int) int { //返回值是一个函数类型
var x int //定义局部变量x
return func(d int) int { //返回的闭包函数 和上面的Adder函数返回函数一一对应
x += d //外部引用变量x会保留上次的值
return x
}
}
func main(){
var f := Adder()
fmt.Print(f(1),"--")
fmt.Print(f(20),"--")
}
//输入一个文件名,有后缀名就返回原文件名,没有后缀则返回带后缀名的文件名
func makeSuffix(suffix string ) func (string) string {
return func (name string) string {
if !strings.HasSuffix(name,suffix) {
return name+suffix
}
return name
}
}
f := makeSuffix()
fmt.Print(f("aa.jpg"),"--")
数组*:
(1) 数组内存中分配:
a. 数组的地址可以通过数组名来获取&Arr
b. 数组的第一个元素的地址就是数组的首地址
c. 数组的各个元素的地址间隔是依据数组类型所占字节数递增而列 比如int 8字节 int32 4字节
(2) 数组使用注意事项和细节(重要):
1. 使用数组步骤:1 申明数组并开辟空间 2 给数组各个元素赋值 3 使用数组
1. 数组是多个相同类型数据的组合,一个数组一旦申明/定义了 其长度是固定的,不能动态变化
2. var arr[]int 这时arr 是一个slice切片
3. 数组中的元素可以是任意数据类型,包括值类型和引用类型,但是不能混用
4. 创建数组后 如果没有赋值 则有默认值 数组类型默认值0,字符串数组默认值"", bool数组默认值false
5. 数组属于值类型,默认是值传递,因此会进行值拷贝.(数组间不会相互影响)
(3) 数组定义: 数组是值类型
a. var a[len] int 比如:var a[5] int * 一旦定义,长度不能变(默认值是0)
b. 长度是数组类型的一部分,因此 var a[6] int 和 var a[10] int 是不同的类型
c. 数组可以通过下标进行访问,下标从0开始,最后一个元素下标是: len-1
for i := 0; i < len(a); i++ {}
for index,value := range a {}
1. index是数组的下标,value是该下标位置的值
2. 它们都仅是在for循环内部可见的局部变量
3. 遍历数组元素的时候,如果不想使用下标index,可以直接把下标index标为下划线_
d. 访问越界,如果下标在数组合法范围外,则会触发访问越界,会panic
e. 数组是值的类型,因此改变副本的值,不会改变本身的值
var a [5] int
b := a //b 是 a 的副本
b[0] = 100 //改变数组b的值
fmt.Println(a) //a的值不变 [0 0 0 0 0]
fmt.Println(b) //b的值会变 [100 0 0 0 0]
(4) 初始化:
a. var arr1 [5]int = [5]int{1,2,3}
b. var arr2 = [5]int{1,2,3,4,5}
c. var arr3 = [...]int{1,2,3,4,5,6} //[...] 让系统自己判断数组的列个数
d. var str = [5]string{3:"hello",4:"tom"} // 可以指定元素数组的下表来初始化数组的值 第4个元素值是hello
(5) 多维数组:
a. var age [5][3]int
b. var f [2][3]int = [...][3]int{{1,2,3},{7,8,9}} //2个子数组,每个子数组有3个值
切片*:
(1) 切片是数组的一个引用,因此切片是引用类型
(2) 切片的长度可以改变,因此切片是一个可变的数组
(3) 切片遍历方式和数组一样,长度用len()求长度
(4) cap可以求出slice的最大容量, 0<=len(slice)<=cap(array) 其中array是slice引用的数组
(4.1) 从slice底层来说,其实切片就是一个数据结构(struct结构体)
a. 切边内存包含3个空间 引用到数据的那个下标的指针,len , cap
(5) 切片的定义: var 变量名 []类型, 比如: var str []string var arr []int
*切片使用的三种方式:
1. 定义一个切片,然后让切片去引用一个创建好的数组
var arr[5]int = [...]int{1,2,3,4,5}
var slice []int
slice = arr[2:3] //slice是切片名 arr[2:3] 表示slice引用到arr这个数组 从下标2到3结束但不包含3
fmt.Printf("切片的地址%p\n",slice) //输出:切片的地址0x120100bc
fmt.Printf("数组第三个元素地址%p\n",&a[2]) //输出:数组第三个元素地址0x120100bc
2. 通过make来创建切片
var slice []int = make([]int,4,10) //make来创建一个切片 (slice := make([]type,len,cap)).
fmt.Println(slice) //默认值是0
fmt.Printf("len=%v cap=%v 地址=%p",len(slice),cap(slice),&slice) //len=4 cap=10 地址=0xc00005a2c0
slice[2] = 200
总结: a. 通过make方式创建切片可以指定切片的大小和容量
b. 如果没有给切片赋值,那么就胡使用默认值[int float =>0 string=>"" bool=>false]
c. 通过make方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素
3. 定义一个切片,直接就指定具体数组,使用原理类型make的方式
var slice []int = []int {1,3,5}
fmt.Println(slice)
(6) 切片扩容(追加)
用append内置函数,可以对切片进行动态追加
1. 通过append 直接给slice追加具体元素
var slice []int = []int{100,200}
slice = append(slice,300,400)
fmt.Println(slice) //[100,200,300,400]
2. 通过append将切片slice追加给slice
slice = append(slice,slice...) //固定写法
切片append操作的底层原理分析:
1. 切片append操作本质就是对数组扩容
2. go底层会创建一个新的数组newArr(扩容后的大小)
3. 将slice原来包含的元素拷贝到新的数组newArr
4. slice重新引用到newArr
5. 注意newArr 是在底层维护的,程序员不可见
(7) 切片拷贝
var a[]int = []int{1,2,3,4,5}
b := make([]int, 10) //make 一个切片
copy(b,a)
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5 0 0 0 0 0]
说明: copy(para1,para2) :para1和para2都是切片类型
(8)string与切片
str := "hello go"
s1 := str[0:5]
s2 := str[5:]
排序*:
(1) 引入包 import "sort"
(2) 数组排序:
a.func arrSort(){
var arr = [...]int{1,33,5,99,2}
sort.Ints(a[:]) //因为数组是值类型 所以要传入切片
fmt.Println(a)
}
b. var s = [...]string{"as","eee","rrt"}
sort.Strings(s[:]) //对字符串进行排序
c. var f = [...]float64{0.8,0.2,99,8.2}
sort.Float64s(f[:]) //对浮点数进行排序
(3) search 检索
s := [...]int{3,1,6,5}
sort.Ints(s[:]) //检索key前先要排序 不然search会不准
index := sort.SearchInts(s[:],3) //返回值3的key
map*: key-value的数据结构,又叫字典或关联数组
(1) 注意: slice,map,还有function不可以用做key, 因为这几个没法用==判断
(1) 声明: (申明是不会分配内存的 初始化需要make)
var a map[string]string //key是字符串value是字符串
var a map[string]int //key是字符串value是int
var a map[string]map[string]string //key是字符串value是map
注意:map申明是不会分配内存的,初始化需要make,分配内存后才能赋值和使用
总结: a. map在使用前一定要make
b. map的key是不能重复的.map的value可以重复
c. map的key-value是无序的
(2) map的使用:
1. 方式1:
var a map[string]string
a = make(map[string]string,10)
a["one"] = "张三"
a["two"] = "李四"
2. 方式2:
m := make(map[string]string,10)
m["no1"] = "北京"
m["no2"] = "上海"
3. 方式3:
a := map[string]string{ //申明初始化赋值
"a":"qq",
"b":"ww",
}
(3) map的增删改查
a. map的增加和更新
map["key"] = value //如果key不存在,就是新增,如果key不存在就是修改
b. map的删除
m := make(map[string]string,10)
m["no1"] = "北京"
m["no2"] = "上海"
delete(m,"no1") //删除key是no2的值 delete是内置函数 当指定key不存在,删除操作也不会报错
1) 如果希望一次性删除所有的key
a. 遍历所有的key,逐一删除
*map遍历只能用for-range的结构遍历
for k,v := range m {
fmt.Printf("k =%v v=%v\n",k,v)
}
b. 直接make一个新的空间
citis = make(map[string]string)
fmt.Println(citis)
c. map的查找
val, ok := m["no1"] //val 为map对应key的value , ok 为查找返回结果bool类型
if ok { fmt.Printf("有no1的key 值为%v\n",val) }
(4) map切片 []map
基本介绍: 切片数据类型如果是map,则我们称为slice of map切片,这样使用则map个数就可以动态变化了
案例:
var mons []map[string]string //申明一个map切片
mons = make([]map[string]string,2) //切片也要make分配内存
// 给map切片赋值2个
if mons[0] == nil {
mons[0] = make(map[string]string,2) //map赋值钱需要make
mons[0]["name"] = "张三"
mons[0]["age"] = "20"
}
if mons[1] == nil {
mons[1] = make(map[string]string,2)
mons[1]["name"] = "李四"
mons[1]["age"] = "24"
}
// 定义一个新的map
newMons := map[string]string{ "name" = "王武","age" = "30",}
mons = append(mons,newMons)
fmt.Println(mons)
(5) map排序:
a. golang中的map默认是无序的,每次遍历得到的输出可能不一样
b. golang中map的排序,是现将key进行排序,然后根据key值遍历输出即可
1. 现将map的key放入到切片中,再对切片排序,遍历切片,然后再按key输出map
map1 := map[int]int{
10:100,
3:44,
7:77,
}
var keys []int
for k,_ := range map1{
keys = append(keys,k)
}
sort.Ints(keys)
for _,k := range keys{
fmt.Sprintf("map1[%v]=%v\n",k,map1[k])
}
(6) map的使用细节:
a. map是引用类型,遵循引用类型的机制,在一个函数接收map,修改后会直接修改原来的map
b. map的容量达到后,想要map增加元素,会自动扩容,并不会发送panic,也就是说map能自动增长键值对(key-value)
c. map的value也经常使用struct类型,更适用于管理复杂的数据
锁*sync*:
https://blog.zxysilent.com/post/goweb-03-6.html
(1)线程同步 (sync 同步包)
a. 互斥锁, var mu sync.Mutex
b. 读写锁, var mu sync.RWMutex
import ("fmt";"sync";"time";)
var wg sync.WaitGroup //WaitGroup 可以用来等待协程执行完成
func fn1(){ time.Sleep(time.Second*1);fmt.Println("sleep 1 s");
wg.Done() // 执行完成就关闭一个等待
}
func fn2(){ time.Sleep(time.Second*1);fmt.Println("sleep 2 s");
wg.Done() // 执行完成就关闭一个等待
}
func main(){
begin := time.Now() //开始时间
for i:=0;i<5 ;i++ {
go fn1()
wg.Add(1) // 每次启动一个协程就添加一个进入同步组
go fn2()
wg.Add(1)
}
wg.Wait() //等待所有子协程执行完成后才继续执行下面代码
end := time.Now()
fmt.Println("总共用时:",end.Sub(begin))
}
(2)go get 安装第三方包
go get github://。。。。
结构体*:(和 java/PHP 语言的对象class 类似)
(1) 结构体特点:
a. 去掉了传统oop语言的继承,方法重载,构造函数和析构函数,隐藏的this指针等等
a. 用来定义复杂数据结构
b. struct里面可以包含多个字段
c. struct类型可以定义方法,注意和函数的区分
d. struct类型是值类型,也可以嵌套
e. Go语言没有class类型,只有struct类型
(2) 结构体的内存分配机制:
a. 变量总数分配在内存中的
(3) 结构体定义:
a. 结构体申明:
type 结构体名称(自定义) struct {
field1 type
field2 type
}
例如: type Student struct{ // 结构体名称如果首字母大写则在其他包和本包使用
Name string // 属性首字母大写也一样可以在其他包使用 (public)
Age int // 属性一般是基本数据类型,数组,也可以是引用类型
}
b. 创建结构体变量和访问结构体字段
1) var stu Student
2) var stu1 Student = Student{"张三",20}
stu2 := Student{"张三",20}
2) var stu3 *Student = new (Student) //申明
//因为stu3是一个指针,因此标准的字段赋值的方式是:
(*stu3).Name = "小王" //赋值 [标准的写法]
(*stu3).Age = 30 //(*stu3).Age = 30 等价于 stu3.Age = 30 原因: go的设计者为了使用方法在底层对
stu3.Age = 30 // stu3.Age = 30进行处理 会对stu3加上取值运算(*stu3).Age = 30
5) var stu5 *Student = &Student{"mary",45} //申明并给属性赋值
4) var stu4 *Student = &Student{}
// stu4是一个指针
(*stu4).Age = 11 //标准写法
stu4.Name = "tom" //简单写法
*stu4.Age = 11 //会报错! 原因是.的运算优先级会高于*
c. 结构体初始化:
1) var stu Student
stu.Name = "jack"
stu.Age = 20
2) var stu2 = Student{
Age:20,
Name:"jack",
}
fmt.Printf("Name:%p\n",&stu2.Name) //打印Name的地址
d. 结构体的使用注意事项和细节:
1. 结构体type重新定义(相当于取别名),golang认为是新的数据类型,但是相互间可以强转
type integer init
func main(){
var i integer =10
var j int =20
//j=i //会报错
j = integer(i)
}
2. 在结构体的每个字段上,可以写一个tag,该tag可以通过反射机制获取,常见的使用场景是序列化和反序列化
// 序列化 是返回一个字符串json到客户端
type Monster struct{ //申明一个结构体
Name string `json:"name"` //`json:"name"` 就是结构体标签 struct tag 作用在反射结果json后可以使Name小写成name
Age int `json:"age"`
Skill string `json:"skill"`
}
func main(){
// 1 创建一个monster变量
monster := Monster{"牛魔王",200,"芭蕉扇"}
//2 将moster变量序列化为一个josn字符串
jsonStr,err := json.Marshal(monster) //json.Marshal 函数中用到反射
if err != nil{
fmt.Println("json 错误处理,",err)
}
fmt.Println("jsonStr:",string(jsonStr))
}
方法*:
(1) 基本介绍:
Golang 中的方法是作用在指定的数据结构上的(即:和指定的数据类型绑定),因此自定义类型都可以有方法,而不仅仅是struct
(2) 方法的申明和调用
type Person struct{ Name string }
//给Person这个结构体绑定一个test方法
func (p Person) test(){ //表示Person这个结构体有一个方法,方法名是test (类型php里的class类里有一个方法)
fmt.Println(a.Num)
}
func main(){
var p Person //申明一个结构体变量
p.Name = "tom" //给这个结构体变量赋值
p.test() //调用这个结构体里绑定的一个方法
}
说明: a. func (p Person) test(){ //表示A这个结构体有一方法,方法名是test
b. (p Person) //体现 test方法是A类型绑定的 ,表示哪个Person变量调用,这个p就是它的副本(类型函数传参)
c. p.test() //不能直接test()调用和用其他类型结构体调用,只能本所绑定的结构体来调用
d. p是可以自定义的
(3) *方法的调用和传参机制原理:
a. 通过一个变量取调用方法时,其调用机制和函数一样
b. 不一样的是 变量调用方法时, 该变量本身也会作为一个参数传递到方法(如果变量是值类型 则是值拷贝,如果变量是引用类型 则进行地址拷贝)
(4) 方法注意事项和细节:
a. 结构体类型是值类型,在方法调用中,遵循值类型的传递机制,是只拷贝传递方式
b. 如果想在方法中修改结构体变量的值,可以通过结构体指针的方式来处理
type Circle struct{ radius float64 }
func (c *Circle) area() float64 { // 为了提高效率 通常结构体方法定义是指针类型
c.radius = 10.0 //c.radius 等价于 (*c).radius
return 3.14 * c.radius * c.radius
}
func main(){
var c Circle
c.radius = 7.0
res := c.area()
fmt.Println(c.radius) //10
fmt.Println(res)
}
c. Golang中的方法作用在指定的数据类型上(即:和指定的数据类型绑定),因此自定义类型都可以有方法,而不仅仅是struct,比如int,float64等都可以有
type integer int
func (i *integer) printt() {
*i = *i + 1
}
func main(){
var i integer = 55
i.printt()
fmt.Println(i) //56
}
d. 方法的访问范围控制规则和函数一样,方法名首字母小写,只能在本包访问,方法名首字母大写,可以在本包和其他包访问
e. 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
type Student struct {
Name string
Age int
}
func (stu *Student) string() string {
str := fmt.Sprintf("Name=[%v] Age=[%v]",stu.Name,stu.Age)
return str
}
func main(){
stu := Student{
Name : "TOM",
Age : 22,
}
fmt.Println(&stu)
}
面向对象*:
(1) 面向对象编程实例
步骤: 申明定义结构体 编写结构体字段 编写结构体方法
type Student struct{
name string
gender string
age int
id int
score float64
}
func (s *Student) say() string {
inforStr := fmt.Sprintf("student的信息:name=[%v],gender=[%v],age=[%v],id=[%v],score=[%v]",
s.name,s.gender,s.age,s.id,s.score)
return inforStr
}
func main(){
//创建一个实例
var stu = Student{
name : "tom",
gender : "male",
age : 18,
id : 1000,
score : 99.0,
}
str := stu.say()
fmt.Println(str)
}
(2) 工厂模式:
说明:Golang的结构体没有构造函数,通常可以使用工厂模式解决这个问题
看一个需求:
package model
type Student struct{
Name string ...
}
详解: 因为这里的Student的首字母S是大写的,如果我们想在其他包创建Student的实例(比如main包),引入model包后,就可以直接创建Student结构
体的实例。但是如果首字母是小写,比如是type student struct{...}就不行了 怎么办--->工厂模式来解决。
例子:
student.go:
package model
//定义一个结构ti
type student struct{ //student首字母小写 外部包不能直接访问
Name string //首字母大写 外部包可以直接访问
score float64 //首字母小写 外部包不能直接访问
}
//定义一个工厂可外部访问的函数
func NewStudent(n string, s float64) *student {
return &student{
Name : n,
score : s,
}
}
//如果score字段首字母小写,则在其他包不可以直接访问,我们可以提供一个方法在其他包访问小写字母的字段
func (s *student) GetScore() float64 {
return s.score
}
main.go :
package main
import "go_code/day6/code/factory/model"
func main(){
//定义的student结构体是首字母小写,我们可以通过工厂模式解决
var stu = model.NewStudent("tom~~",88.8)
fmt.Println("Name=",stu.Name , "score=",stu.GetScore())
}
(3) 面向对象的三大特性:
说明:Golang语言也又面向对象的继承,封装,多态的特性,只是实现方式和其他oop语言不一样
如何理解抽象:定义一个结构体的时候,实际上是把一类事物的共有的属性和方法提出出来,形成一个屋里模型(结构体),这种研究问题的方法称为抽象
a.封装:就是吧抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只能通过被授权的方法才能对字段进行操作
封装的实现步骤:
1. 将结构体,字段的首字母小写(其他包不能访问,类似private)
2. 将结构体所在包提供一个工厂模式的函数,首字母大写,类似一个构造函数
3. 提供一个首字母大写的Set方法(类似其他语言的Public),用于对属性赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//数据验证逻辑
var 字段 = 参数
}
4. 提供一个首字母大写的GetXxx方法(类似public) 用于获取属性的值
func (s *student) GetScore() float64 {
return s.score
}
b. 继承:在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
基本语法:
type Goods struct{
Name string
Price int
}
type Book struct{
Goods //这里嵌套匿名结构体Goods 相当于继承了Goods结构体的属性和方法
Writer string
}
继承的深入讨论:
1) 结构体可以使用嵌套结构体所有的字段和方法. 即:首字母大写或者小写的字段,方法,都可以使用
type A struct{
Name string
age int
}
func (a *A) SayOk(){ //A 结构体的方法 SayOK
fmt.Println("A SayOk",a.Name)
}
func (a *A) hello(){ //A 结构体的方法 hello
fmt.Println("A hello",a.Name)
}
type B struct{ //相当于B 是 A 的子类
A
}
func main(){
var b B
b.A.Name = "tom"
b.A.age = 22
b.A.SayOk() //A SayOk tom // 简化: b.SayOk()
b.A.hello() //A hello tom //小写的方法也可以调用
}
2) 匿名结构体访问可以简化:
var b B
b.a.name = "tom" ===> b.name= "tom"
b.a.say() ===> b.say()
3) 当结构体和嵌套的匿名结构体有相同的字段或方法时,编译器采用就近访问原则,如果希望访问匿名结构体的字段和方法,
可以通过匿名结构体名来区分 即:b.A.hello() 明确是A结构体的方法 b.hello() 则优先访问B结构体的hello方法
4) 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段或方法(同时结构体本身没有同名的字段和方法),在访问时,
就必须明确指定匿名结构体名字,否则编译报错
type A struct{
Name string
Age int
}
type B struct{
Name string
score int
}
type C struct{
A
B
// Name string
}
func main(){
var c C
c.Name = "tom" //报错 必须 c.A.Name 或 c.B.Name
}
5) 如果一个结构体嵌套了一个有名结构体,这种模式就是组合。如果是组合关系,那么在访问组合的结构体的字段或方法时,
必须带上结构体名字
type A struct{
Name string
}
type C struct{
a A //相当于给嵌套的A 结构体取了一个别名
}
func main(){
var c C
c.a.Name = "jack" //访问有名结构体字段时 必须带上有名结构体的名字
}
6)嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
type Good struct{
Name string
Price float64
}
type Brand struct{
Name string
Address string
}
type TV struct{
Goods
Brand
}
type TV2 struct{
*Goods //也可以嵌套指针类型
*Brand
}
func main(){
//嵌套匿名结构体后,也可在创建结构体实例时,直接指定各个匿名结构体字段的值
tv := TV{ Goods{"电视机01",4999.00},Brand{"海尔","山东"}, }
tv2 := TV{
Goods{
Name : "电视机02",
Price : 5999.00,
},
Brand{
Name : "夏普",
Address : "北京",
},
}
tv3 := TV2{ &Goods{"电视机03",6999.00}, &Brand{"创维","湖北"}, } //指针类型用&
fmt.Println(tv)
fmt.Println(tv2)
fmt.Println("tv3",*tv3.Goods, *tv3.Brand)
}
接口*(interface): 多态性主要通过接口来体现
基本介绍: interface类型可以定义一组方法,但这些方法不需要实现.且interface不能包含任何变量。到某个自定义类型(比如结构体Phone)要
使用的时候,根据具体情况把这些方法实现
基本语法:
type 接口名 interface{
method1(参数列表) 返回值列表
method2(参数列表) 返回值列表
}
小结说明:
1) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想
2) Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型的所有方法,那么这个变量就实现了这个接口。因此
Golang中没有implement这样的关键字
注意事项和细节:
1) 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的实例
type Usb interface{ Start();Stop(); }
type Phone struct{}
type Camera struct{}
func (p Phone) Start(){fmt.Println("手机开始工作..")}
func (p Phone) Stop(){fmt.Println("手机停止工作..")}
func (p Camera) Start(){fmt.Println("相机开始工作..")}
func (p Camera) Stop(){fmt.Println("相机停止工作..")}
type Computer struct{}
//编写一个work方法,接受一个Usb接口类型变量 实现了Usb接口
func (c Computer) Working(usb Usb){
//通过usb这个接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}
func main(){
//先创建结构体实例
computer := Computer{}
phone := Phone{}
camera := Camera{}
computer.Working(phone)
computer.Working(camera)
}
2) 接口中所有的方法都没有方法体,即都是没有实现的方法
3) 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
4) 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
5) 一个自定义类型可以实现多个接口
type AInterface interface{ Say() }
type BInterface interface{ Hello() }
type Monster struct{} //Monster 结构体实现了AInterface和BInterface
func (m Monster) Say(){ fmt.Println("Monster Hello()") }
func (m Monster) Hello(){ fmt.Println("Monster Hello()") }
func main(){
var monster Monster
var a2 AInterface = monster
var b2 BInterface = monster
a2.Say()
b2.Hello()
}
6) Golang接口中不能有任何变量
7) 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现
type BInterface interface{ test01() }
type CInterface interface{ test02() }
type AInterface interface{ //AInterface 继承了 BInterface 和 CInterface
BInterface
CInterface
test03()
}
//如果需要实现AInterface,就需要将BInterface和CInterface的方法都实现
type Stu struct{ }
//必须要实现所有方法
func (stu Stu) test01(){}
func (stu Stu) test02(){}
func (stu Stu) test03(){}
func main(){
var stu Stu
var a AInterface = stu
var b BInterface = stu
var c CInterface = stu
a.test01() //a可以调 test01 test02 test03 三个方法
b.test01() //b只能调 本身的test01方法
c.test02() //c只能调 本身的test02方法
}
8) interface类型默认是一个指针(引用类型),如果没有interface初始化就使用,那么会输出nil
9) 空接口interface{} 没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口
接口编程的最佳实践:
import (
"fmt"
"sort"
"math/rand"
)
//1申明Hero结构体
type Hero struct{
Name string
Age int
}
//2申明Hero结构体切片类型
type HeroSlice []Hero
//3 实现Interface 接口
func (hs HeroSlice) Len() int { return len(hs) }
// Less 方法决定使用什么标准进行排序
func (hs HeroSlice) Less(i,j int) bool {
return hs[i].Age < hs[j].Age
//也可以改为对Name排序
//return hs[i].Name < hs[j].Name
}
func (hs HeroSlice) Swap(i,j int) { hs[i], hs[j] = hs[j], hs[i] } //交换
func main(){
var intSlice = []int{0,-1,22,7,99,66}
sort.Ints(intSlice) //对切片进行排序
fmt.Println(intSlice)
//对结构体切片排序
var heroes HeroSlice
for i:=0;i<10;i++{
hero := Hero{
Name : fmt.Sprintf("英雄|%d", rand.Intn(100)),
Age : rand.Intn(100),
}
//将hero append 到heroes切片
heroes = append(heroes,hero)
}
//看看排序前的顺序
for _,v := range heroes {
fmt.Println(v)
}
//调用sort.Sort
sort.Sort(heroes)
fmt.Println("排序后~~~~~~~")
//看看排序后的顺序
for _,v := range heroes {
fmt.Println(v)
}
}
实现接口VS 继承:
继承的价值主要在于:解决代码的复用性和可维护性
接口的价值主要在于:设计好各种规范,让其他自定义类型取实现这些方法
接口比继承更灵活
接口比继承更加灵活,继承是满足 is - a 的关系, 而接口只需满足like - a 的关系
接口在一定程度上实现代码解耦
多态:
基本介绍:实例具有多种形态,面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可以按统一的接口来调用不同
的实现。这时接口实例就呈现不同的形态
类型断言: 由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言。
类型断言判断案例: 如何在进行断言时,带上检测机制,如果成功ok,否则也不要抱panic
var x interface{}
var b float32 = 1.1
x = b //空接口可以接收任意类型
y,ok := x.(float64) //x ==>float32 [使用类型断言]
if ok {
fmt.Println("convert success...")
fmt.Printf("y 的类型是%T 值是%v\n",y,y)
}else{
fmt.Println("convert fail...")
}
fmt.Println("go........")
类型断言最佳案例: [!!!注意体会]
type Usb interface{ Start(); Stop();}
type Phone struct{name string}
//让Phone 实现 Usb接口的方法 并额外实现一个Call方法
func (p Phone) Start(){fmt.Println("手机开始工作..")}
func (p Phone) Stop(){fmt.Println("手机停止工作..")}
func (p Phone) Call(){fmt.Println("手机开始打电话..")}
type Camera struct{name string}
func (p Camera) Start(){fmt.Println("相机开始工作..")}
func (p Camera) Stop(){fmt.Println("相机停止工作..")}
type Computer struct{}
//编写一个Working方法,接受一个Usb接口类型变量 通过usb这个接口变量来调用Start和Stop方法
func (c Computer) Working(usb Usb){
usb.Start()
//如果usb是指向Phone的结构体变量,则还需要调用Call方法
if phone,ok := usb.(Phone); ok { //类型断言 [!!!注意体会]
phone.Call()
}
usb.Stop()
}
func main(){
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
var computer Computer
for _,v := range usbArr {
computer.Working(v)
fmt.Println()
}
}
文件* : 文件是数据源(保存数据的地方) -----> os.File
基本介绍: 文件在程序中是以流的形式来操作的
流: 数据在数据源(文件)和程序(内存)之间经历的路径
输入流: 数据从数据源(文件)到程序(内存)的路径 [读文件]
输出流: 数据从程序(内存)到数据源(文件)的路径 [写文件]
a.读文件操作:
(1) 打开文件和关闭文件:
func Open(name string) (file *File, err error)
func (f *File) Close() error
example:
import ( "fmt";"os";)
func main(){
file , err := os.Open("d:/go.go") //open file
if err != nil { fmt.Println("open file err=",err) }
fmt.Printf("file=%v",file)
err = file.Close() //close file
if err != nil { fmt.Println("close file err=",err)}
}
(2) 读取文件内容并显示在终端(带缓冲的方式 [适合大文件] ): 使用os.Open , file.Close ,bufio.NewReader() , reader.ReadString 函数和方法
import ( "fmt";"os";"bufio";"io"; )
func main(){
file , err := os.Open("d:/go.go") //open file
if err != nil { fmt.Println("open file err=",err) }
defer file.Close() //timeliness close file
reader := bufio.NewReader(file) //创建一个Reader 带缓冲的 适合大文件
for {
str,err := reader.ReadString('\n') //每读到一个换行符就结束
if err == io.EOF { break } //io.EOF 代表文件的末尾 //读取到文件末尾不再继续读文件
fmt.Print(str)
}
fmt.Println("文件读取结束...")
}
(3) 读取文件内容并显示在终端(使用ioutil一次性将整个文件读取到内存中) [适合文件不大的情况].相关方法和函数(ioutil.ReadFile)
import ( "fmt";"io/ioutil";)
func main(){
//使用ioutil.ReadFile 一次性将文件读取到位 不需要显示Open 和 Close 文件
file := "d:/go.go"
content,err := ioutil.ReadFile(file)
if err != nil { fmt.Printf("read file err=%v",err)}
fmt.Printf("%v",string(content)) //[]byte
}
b.写文件操作:
基本介绍:
func OpenFile(name string,flag int,perm FileModel)(file *file ,err error) //name打开文件路径 flag文件打开模式 perm权限控制
(1) 创建一个新文件 写入5句 "hello go"
import ( "fmt";"bufio";"os";)
func main(){
filePath := "d:/abc.txt"
file,err := os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE,0666)
if err != nil { fmt.Printf("open file err=%v\n",err);return;}
defer file.Close() //及时关闭句柄
str := "hello go\r\n"
//写入到缓冲 使用带缓冲的*writer
writer := bufio.NewWriter(file)
for i := 0;i<5;i++{ writer.WriteString(str) }
//因为writer是带缓冲的,因此在调用WriteString方法时 其实内容是线写入到缓冲的 所以需要调用Flush方法
//将缓冲的数据真正写入到文件中,否则文件中会没有数据
writer.Flush()
}
(2) 打开一个存在的文件 将原来的内容覆盖成新的内容 10 句 "你好 祖国!"
file,err := os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE,0666) 替换如下,
file,err := os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC ,0666)
(3) 打开一个存在的文件,将原来的内容追加内容 "追加内容"
file,err := os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC ,0666) 替换如下,
file,err := os.OpenFile(filePath, os.O_WRONLY | os.O_APPEND ,0666)
(4) 将一个文件的内存写入到另一个文件。注:两个文件都存在 使用 ioutil.ReadFile / ioutil.WriteFile
import ( "fmt";"io/ioutil";)
func main(){ //file1Path 数据写到file2Path
file1Path := "d:/abc.txt"
file2Path := "d:/kkk.txt"
data,err := ioutil.ReadFile(file1Path)
if err != nil { fmt.Printf("read file err=%v\n",err) return }
err = ioutil.WriteFile(file2Path,data,0666)
if err != nil { fmt.Printf("Write file err=%v",err) }
}
(5) 统计一个文件中含有的英文,数字,空格以及其他字符数量
import ( "fmt";"os";"io";"bufio";)
type CharCount struct{
ChCount int //英文个数
NumCount int //数字个数
SpaceCount int //空格个数
OtherCcunt int //其他字符个数
}
func main(){
fileName := "d:/abc.txt"
file,err := os.Open(fileName)
if err != nil {fmt.Printf("open file err=%v\n",err);return ;}
defer file.Close()
var count CharCount
reader := bufio.NewReader(file)
for{ //循环读取fileName 内容
str, err := reader.ReadString('\n')
if err == io.EOF{ break }
for _,v := range str{ // 遍历 str 进行统计
switch {
case v >='a' && v <= 'z':
fallthrough
case v >='A' && v <= 'Z':
count.ChCount++
case v ==' ' || v== '\t':
count.SpaceCount++
case v =='0' || v== '9':
count.NumCount++
default:
count.OtherCcunt++
}
}
}
fmt.Printf("英文个数=%v 数字个数=%v 空格个数=%v 其他字符个数=%v",
count.ChCount,count.NumCount,count.SpaceCount,count.OtherCcunt)
}
(6) 编写一段程序,可以获得命令行各个参数 os.Args
func main(){
fmt.Println("命令行的参数",len(os.Args))
for i,v := range os.Args {
fmt.Printf("Args[%v]=%v\n",i,v)
}
}
go build -o test.exe
test.exe tom d:/aaa/bbb/init.log 999 //参数以空格分隔放在切片里
// Args[0]=test.exe
// Args[1]=tom
// Args[2]=d:/aaa/bbb/init.log
// Args[3]=999
(7) flag* 包来解析命令行参数:
func main(){
var user string;var pwd string;var host string;var port int;
flag.StringVar(&user,"u","","用户名默认为空")
flag.StringVar(&pwd,"pwd","","密码默认为空")
flag.StringVar(&host,"h","localhost","主机名默认为localhost")
flag.IntVar(&port,"port",3306,"端口号默认为3306")
flag.Parse() //转换解析
fmt.Printf("user=%v pwd=%v host=%v port=%v",user,pwd,host,port)
}
go build -o test.exe main.go //编译文件
test.exe -pwd 123456 -h 127.0.0.1 -u jack //user=jack pwd=123456 host=127.0.0.1 port=3306
c.判断文件是否存在:
基本介绍:os.Stat() (1)返回错误为nil,说明文件或文件夹存在 (2)返回错误类型使用os.isNotExist()判断为true,说明文件或文件夹不存在
(3)返回错误为其他类型,则不确定是否存在
// 基于以上三个错误类型 自己定义了一个函数
func PathExists(path string)(bool,error){
_,err := os.Stat(path)
if err == nil { //文件或目录存在
return true,nil
}
if os.isNotExist(err){
return false,nil
}
return false,err
}
json*序列化:
(1) 将结构体,map,切片 进行序列化 ---> import "encoding/json"
a.//结构体序列化
type Monster struct { Name string `json:"name"`;Age int;Birthday string;Sal float64;skill string; }
func testStruct(){
monster := Monster{
Name : "牛魔王",
Age : 500,
Birthday : "1880-11-22",
Sal : 8000.0,
skill : "风沙走石",
}
data,err := json.Marshal(&monster) // monster 序列化
if err != nil { fmt.Println("序列化失败",err) }
fmt.Printf("序列化后的结构%v\n",string(data))
}
b. //Map 序列化
func testMap(){
var a map[string]interface{}
a = make(map[string]interface{})
a["name"]="红孩儿";a["age"]=15;a["address"]="火云洞";
data,err := json.Marshal(a)
if err != nil { fmt.Println("序列化错误",err) }
fmt.Printf("序列化后的结构%v\n",string(data))
}
c. //切片序列化
func testSlice(){
var slice []map[string]interface{}
var m1 map[string]interface{}
m1 = make(map[string]interface{})
m1["name"] = "jack"; m1["age"] = 10; m1["address"] = "上海";
slice = append(slice,m1)
var m2 map[string]interface{}
m2 = make(map[string]interface{})
m2["name"] = "tom"; m2["age"] = 19; m2["address"] = [2]string{"长沙","武汉"}
slice = append(slice,m2)
data,err := json.Marshal(slice)
if err != nil { fmt.Println("序列化错误",err) }
fmt.Printf("序列化后的结构%v\n",string(data))
}
(2) 反序列化: ----> json.Unmarshal([]byte(str),&monster)
a. //反序列化为结构体
type Monster struct { Name string `json:"name"`;Age int;Birthday string;Sal float64;skill string; }
func unmarshalStruct() {
str := "{\"name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"1880-11-22\",\"Sal\":8000}"
var monster Monster
err := json.Unmarshal([]byte(str),&monster)
if err != nil { fmt.Println("反序列化错误",err) }
fmt.Printf("反序列化后 monster=%v monster.Name=%v\n",monster,monster.Name)
}
b. //反序列化为map
func unmarshalMap() {
str := "{\"address\":\"火云洞\",\"age\":15,\"name\":\"红孩儿\"}"
var map1 map[string]interface{}
//反序列化map的时候不需要make,因为make操作封装到Unmarshal函数
err := json.Unmarshal([]byte(str),&map1)
if err != nil { fmt.Println("反序列化错误",err) }
fmt.Printf("反序列化后 monster=%v \n",map1)
}
c. //反序列化为Slice
func unmarshalSlice() {
str := "[{\"address\":\"上海\",\"age\":10,\"name\":\"jack\"},"+
"{\"address\":[\"长沙\",\"武汉\"],\"age\":19,\"name\":\"tom\"}]"
var slice []map[string]interface{}
//反序列化Slice的时候不需要make,因为make操作封装到Unmarshal函数
err := json.Unmarshal([]byte(str),&slice)
if err != nil { fmt.Println("反序列化错误",err)
fmt.Printf("反序列化后 monster=%v \n",slice)
}
单元测试*
基本介绍: go语言自带一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。testing框架和其他语言中的测试框架类似,
可以基于这个框架写针对相应的测试应用,也可以基于框架写相应的压力测试用例,通过单元可是可以解决如下问题:
1) 确保每个函数都是可运行的,并且结果是正确的
2) 确保写出来的代码性能是好的
3) 单元测试能及时的发现程序设计或实现的逻辑错误,是问题早暴露,便于问题的定位解决。而性能测试的重点在于程序设计上的一些问题,让程序在高并发的情况下还能稳定
单元测试细节:
1 测试用例文件名必须以 _test.go结尾.比如cal_test.go ,
2 测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名。比如TestAddUpper
3 TestAddUpper(t *testing.T)的形参类型必须是 *testing.T
4 一个测试用例文件中,可以有多个测试用例函数.比如 TestAddUpper TestSub
5 运行测试用例指令
a. cmd>go test [如果运行正确,无日志,错误时,会输出日志]
b. cmd>go test -v [运行正确或是错误,都输出日志]
6 当出现错误时,可以使用t.Fatalf来格式化输出错误信息,并退出程序
7 t.Logf 可以输出相应的日志
8 测试用例函数,并没有放在main函数中,但也执行了,这就是测试用例的方便之处
9 PASS表示测试用例运行成功,FALL表示测试用例失败
10 测试单个文件,一定要带上被测试的源文件 go test -v cal_test.go cal.go //cal被测试的源文件
11 测试单个方法 go test -v -test.run TetsAddupper
goroutine*
(1) goroutine基本介绍:
a. 进程和线程说明:
1) 进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
2) 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单元
3) 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
4) 一个程序至少有一个进程 一个进程至少有一个线程
b. 并发和并行:
1) 多线程程序在单核上运行,就是并发 ---> 多个任务作用在一个cpu,在微观角度,在同一个时间点上其实只有一个任务在执行
2) 多线程程序在多核上运行,就是并行 ---> 多个任务作用在多个cpu同时进行
(2) go协程和go的主线程:
1) go主线程(有程序员称为线程/也可以理解位就是进程):一个Go线程上,可以起多个协程,可以理解,协程就是轻量级线程。
2) Go协程的特点*:有独立的栈空间,共享程序堆程序,调度由用户控制,协程(goroutine)是轻量级线程
(3) goroutine案例:
1) 主线程是一个物理线程,直接作用在cpu上.是重量级的 非常耗费cpu资源
2) 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
3 Golang的协程机制是重要的特点,开启过多的线程 资源消耗大,这里就凸显Golang在并发上的优势
(4) goroutine的调度模型: MPG模式
MPG模式基本介绍: M:操作系统主线程(是物理线程) P: 协程执行需要上下文环境(需要的资源,内存空间等) G:协程
(5) 设置Golang 运行的cpu数
import ( "fmt"; "runtime"; )
func main(){
cpuNum := runtime.NumCPU() // 返回本地机器的逻辑CPU个数
fmt.Println("cpuNum=",cpuNum)
runtime.GOMAXPROCS(cpuNum-1) // 设置可同时执行的最大CPU数 1.8版本以后不需要设置
}
chanel*
(1) channel(管道)-基本介绍:
1) channel本质上就是一个数据结构-队列
2) 数据是先进先出 【FIFO】
3) 线程安全,多goroutine访问时 不需要加锁,就是说channel本身是线程安全的[多个协程操作同一个管道时,不会发生资源竞争问题]
4) channel时有类型的,一个string的channel只能存放string类型数据
(2) 定义/声明 channel
1)var 变量名 chan 数据类型
var intChan chan int (intChan 存放int数据的管道变量名)
2) 说明:
a. channel是引用类型
b. channel必须初始化才能写入数据,即make后才能使用
c. 管道是有类型的,intChan 只能写入int
(3) 管道的初始化 读 写
func main() {
var intChan chan int //申明
intChan = make(chan int, 3) //初始化 容量是3 写入不能超过3
fmt.Printf("值=%v 地址=%p\n",intChan,&intChan) //管道是引用类型
intChan <- 10 //向管道写入数据
num := 10
intChan <- num
intChan <- 11
fmt.Printf("管道长度=%v 容量=%v\n",len(intChan),cap(intChan))
//从管道取取数据
num2 := <-intChan // 给管道的第一个数据取出赋值给变量num2
intChan <- 14 // 先进先出 只能取出一个后再才能像写入一个
fmt.Printf("管道长度=%v 容量=%v num2=%v\n",len(intChan),cap(intChan),num2)
}
注意事项和细节: a. channel只能存放指定类型的数据类型
b. channel数据放满后就不能再放入了,如果再从channel取出数据后,可以继续放入
c. 再没有使用协程的情况下,如果channel数据取完了,再取,就会报deadlock
(4) 存放任意数据类型的管道
func main() {
//定义一个存放任意数据类型的管道 3个数据
//var allChan chan interface{}
allChan := make(chan interface{},3)
allChan <- 10
allChan <- "tom"
cat := Cat{"小白猫",4}
allChan <- cat
//我们希望获得管道中的第三个元素 则先将前2个推出
<-allChan
<-allChan
newCat := <-allChan
fmt.Printf("newCat=%T , newCat=%v\n",newCat,newCat)
a := newCat.(Cat) //类型断言
// fmt.Printf("newCat.Name=%v",newCat.Name) //错误写法 需要类型断言
fmt.Printf("newCat.Name=%v",a.Name)
}
(5) channel的遍历和关闭:
关闭:使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但仍然可以从该channel读数据
遍历: a. 使用for-range 方式进行遍历
b. 在遍历时,如果channel没有关闭 则会出现deadlock错误
c. 在遍历时,如果channel已经关闭 则会正常遍历数据,遍历完后就会推出遍历
func main() {
intChan := make(chan int,100)
for i := 0; i < 100; i++ { //遍历管道
intChan <- i*2 //放入100个数据到管道
}
close(intChan) //遍历管道前关闭管道 避免出现deadlock
for v := range intChan{ //取的时候 变量管道只能for-range
fmt.Println("v=",v)
}
}
(6) 控制协程在主进程结束之后全部读写完成、
func writeData(intChan chan int) { //write Data
for i := 0; i < 50; i++ {
intChan <- i
fmt.Println("write v=",i)
}
close(intChan)
}
func readData(intChan chan int ,exitChan chan bool) { //read data
for { v,ok := <- intChan
if !ok { break }
fmt.Printf("readData 读到数据=%v\n",v)
}
exitChan <- true //读完管道数据
close(exitChan)
}
func main() {
intChan := make(chan int,50)
exitChan := make(chan bool,1)
go writeData(intChan)
go readData(intChan,exitChan)
for { _,ok := <- exitChan //读出是true则说明协程已经读取完毕
if !ok { break }
}
}
(7) channel 可以申明为只读或者只写 性质
func main() {
var chan1 chan<- int //申明为只写
chan1 = make(chan int,3)
chan1 <- 100
//num := <- chan1 // error
var chan2 <-chan int //申明为只读
num2 := <-chan2
//chan2 <- 20 //error
fmt.Println(num2)
}
(8) 使用*select 解决从管道取数据的阻塞问题
func main() {
intChan := make(chan int,10) //定义一个管道 10个数据 int
for i := 0; i < 10; i++ { intChan <- i }
//定义一个管道 5个数据string
stringChan := make(chan string,5)
for i := 0; i < 5; i++ { stringChan <- "hello" + fmt.Sprintf("%d",i) }
//传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock
//问题是 实际开发中,我们不好确定在哪关闭该管道 可以使用select 方式解决
for {
select {
//注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock 会自动到下一个case匹配
case v := <- intChan :
fmt.Printf("从intChan读取数据%d\n",v)
case v := <- stringChan :
fmt.Printf("从stringChan读取数据%s\n",v)
default:
fmt.Println("都找不到了! 开发者可以加入自己的逻辑")
return
}
}
}
(9) 使用 *recover 捕获管道错误
func sayHello() {
for i := 0; i < 10; i++ { fmt.Println("hello word") }
}
func test() {
defer func(){
//捕获test抛出的panic
if err := recover(); err != nil { fmt.Println("test() 发生错误\n",err) }
}()
var myMap map[int]string
myMap[0] = "golang" //没有make 会panic
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=",i)
time.Sleep(time.Second)
}
}
反射*
(1)基本介绍:
1) 反射可以运行时动态获取变量的各种信息 比如变量的类型(type) 类别(kind)
2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段和方法)
3) 通过反射,可以改变变量的额值,可以调用相关的方法
4) 使用放射,需要import("reflect")
(2)反射重要的函数和概念:
1) 变量,interface{} 和 reflect.Value 是可以相互转换的.这在实际开发中是经常用到的
(3)基本数据类型,interface{},reflect.Value 进行反射的基本操作
func reflectTest01 (b interface{}){
//获取reflect.Type
rTyp := reflect.TypeOf(b)
//获取 reflect.Value
rVal := reflect.ValueOf(b)
//将 reflect.Value 转 int类型 rVal.Int()
nu := 2 + rVal.Int()
//将 reflect.Value 转成 interface{}
iv := rVal.Interface()
//将 interface{} 通过断言转换成需要的类型
num := iv.(int)
}
(4) 结构体类型,interface{},reflect.Value, 进行反射的基本类型操作
func reflectTest01 (b interface{}){
rVal := reflect.ValueOf(b) //获取 reflect.Value
iV := rVal.Interface()
fmt.Printf("iv=%v type=%T \n",iV,iV) //iv={tom 12} type=main.Student
stu,ok := iV.(Student) //需要类型断言
if ok {
fmt.Printf("stu.Name=%v\n",stu.Name) //stu.Name=tom
}
}
type Student struct { Name string; Age int; }
func main() {
stu := Student{
Name : "tom",
Age : 12,
}
reflectTest01(stu)
}
(5) reflect.Value.Kind,获取变量的类别,返回的是一个常量
(6) 通过反射修改变量的值
func reflect1(b interface{}) {
rVal := reflect.ValueOf(b)
fmt.Printf("rVal kind=%v\n",rVal.Kind())
//Elem返回v持有的接口保管的值得Value封装,或有v持有的指针的值的value封装
rVal.Elem().SetInt(20) //反射修改变量的值
}
func main() {
var num int =10
reflect1(&num)
fmt.Println("num=",num) // 输出20
}
TCP编程:
网络编程有两种:
1 TCP socket编程,是网络编程的主流。之所有叫Tcp socket编程,是因为底层是基于Tcp/ip 协议的.比如QQ
2 b/s 结构的http编程,我们使用浏览器取访问服务器时,使用的就http协议,而http协议底层也是tcp socket 实现
端口:
a. 0是保留端口
b. 1-1024是固定端口,又叫做有名端口,即被某些程序固定使用,一般程序员不能使用
22:SSH 远程登录协议 23: telent 使用 21:ftp使用 25:smtp服务使用 80:iis使用 7:echo服务
c. 1025-65535 是动态端口,这些端口 程序员可以使用
d. 可以使用netstat -an 查看本机有哪些端口在监听
Ridis*: (官方文档:http://redisdoc.com/)
一. Golang 操作Redis:
1. 使用第三方开源库 github.com/garyburd/redigo/redis
2. 在使用Redis前 先安装第三方Redis库 在GOPATH路径下执行安装指令:
D:\goproject>go get github.com/garyburd/redigo/redis
注意: 在安装前 确保已经安装并配置了Git.
(1) 添加key-value
import "github.com/garyburd/redigo/redis"
func main() {
c,err := redis.Dial("tcp","localhost:6379") //连接redis
if err != nil { fmt.Println("conn redis faild,",err);return; }
defer c.Close()
_,err = c.Do("Set","key1",888) //添加key-value
if err != nil { fmt.Println(err); return; }
r,err := redis.Int(c.Do("Get","key1")) //获取get key
if err != nil { fmt.Println("get key1 faild,",err); return; }
fmt.Println(r)
}
(2) 操作Hash
_,err = c.Do("HSet","user01","name","汤姆")
r,err := redis.String(c.Do("HGet","user01","name")) //如果存放的是int则使用reids.Int()
(3) 批量Set/Get数据
_,err = c.Do("MSet","name","小明","address","武汉")
r,err := redis.Strings(c.Do("MGet","name","address"))
二. Ridis的五大数据类型:String,Hash,List,Set,zSet(有序集合)
(1) redis 的基本使用: 默认有16个数据库 初始默认值0号库 编号是0-15
a. 添加key-val [set]
b. 查看当前的redis的所有key [keys *]
c. 获取key对应的值 [get key]
d. 切换redis数据库 [select index]
e. 查看当前数据库key-value的数量 [dbsize]
f. 清空当前数据库的key-val和清空所有数据库的key-val [flushdb flushall]
(2) String:二进制安全,除了普通字符串外,也可以存放图片等数据。redis字符串value最大值是512M
a. CURD:
set [存在则修改,不出存则添加] get del
setex key1 10 helloword 设置key1 值是helloword 10秒
mset key value [key valye...] 一次性设置多个key value
mget key1 key2 同时获取多个key对应的value
(3) Hash: 是一个键值对集合 是一个string类型的field和value的映射表
CURD:
hset user1 name jack 添加user1字段 的 name jack
hget user1 name 获取user1字段下的name的值
hgetall user1 获取user1字段下的所有属性的值
hmset user2 name mary age 21 设置user2字段下的name 和 age 属性的值
hmget user2 name age 获取user2 下的name和age的值
hlen user2 统计一个哈希有几个元素
hexists user2 name 查看user2哈希中是否有name这个属性
(4) List: List本质是个链表,List元素是有序的,元素的值可以重复
a. lindex a按照索引下标获得元素(从0到右 编号从0开始)
b. llen key 返回列表key的长度 如果可以不存在,则key被解释为一个空列表 返回0
c.CURD:
lpush city beijing shanghai 向city列表中从左边插入beijing shanghai
lrange city 0 -1 取所有的city列表中的所有值
rpush herolist aa bb cc 从链表的左边插入三个值
lpop herolist 从链表左边弹出一个 返回弹出的值
rpop herolist 从链表右边弹出一个 返回弹出的值
del herolist 删除链表
(5) Set: Ridis的Set是String类型的无序集合,且元素值不能重复
CURD:
sadd emails xx xxx 向集合emails 集合添加2个值
smembers emails 遍历该集合中的所值
sismember 判断是否是成员
srem 删除指定的值
三. Ridis连接池 : 事先初始化一定数量的连接,放入连接池.当Go需要操作Redis时 直接从redis连接池取出连接即可
(1) 核心代码:
var pool *redis.Pool
pool = &redis.Pool{
MaxIdle:8, //最大空闲链接数
MaxActive:0, //表示和数据库的最大链接数,0表示没有限制
IdleTimeout:100, //最大空闲时间
Dial:func()(redis.Conn,error){ //初始化
return redis.Dial("tcp","localhost:6379")
},
}
c := pool.Get() //从链接池中取出一个链接
pool.Close() //关闭连接池 就再不能从连接池中取链接了
(2) 案例:
import "github.com/garyburd/redigo/redis"
var pool *redis.Pool
func init() {
pool = &redis.Pool{
MaxIdle:8,
MaxActive:0,
IdleTimeout:100,
Dial:func()(redis.Conn,error){
return redis.Dial("tcp","localhost:6379")
},
}
}
func main() {
conn := pool.Get()
defer conn.Close()
_,err := conn.Do("Set","name","jack")
if err != nil{ fmt.Println(err); return; }
r,err := redis.String(conn.Do("Get","name"))
if err != nil{ fmt.Println(err); return; }
fmt.Println("r=",r)
}
数据结构*:
a. 数据结构是一门研究算法的学科.要学数据结构就要多多思考如何将生活中遇到的问题,用程序去实现解决
稀疏数组: 当一个数组中大部分元素是0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组
稀疏数组的处理方法:
记录数组一共有几行几列,有多少个不同的值,吧具有不同值得元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
队列:队列是一个有序列表,可以用数组和链表实现 遵循先入先出的原则