切片
切片基本介绍:
- 切片的英文为slice
- 切片是数组的一个引用,因此切片是引用传递,在进行值传递需遵守引用传递规则
- 切片使用和数组类似,遍历,访问切片元素和求切片长度 len(silce) 都一样
- 切片的长度是可变的,因此切片是一个可以动态变化的数组
- 切片定义的基本语法:
var 变量名 类型
|var a []int
此时此刻我想吟诗一首:数组有一种初始化数组的方法为:var a = […]int{1,2,3,4} 此种方式等价于 var a […]int | a = {1,2,3,4},能够进行数组长度自动推导,好的我们继续看切片是什么东东
程序示例:
- 引用数组的切片起始下标为2结束下标为4但不包括4
// 演示切片的使用
package main
import "fmt"
func main(){
// 定义一个数组
var a = [...]int{2,3,4,5,6}
// slice是切片名
// 引用数组的切片起始下标为2结束下标为4但不包括4
// a[2:4] 表示slice切片引用到这个数组
slice := a[2:4]
fmt.Println("数组a的值为:",a)
fmt.Println("slice切片值为: ",slice)
fmt.Println("slice切片长度为: ",len(slice))
// cap(切片) 可以用来求切片容量,一般为切片长度的1倍或1.5倍,不固定,
// 随着切片长度增大,容量会动态增加,不需要关心
fmt.Println("slice切片容量为: ",cap(slice))
}
切片在内存中的形式:
- 切片在内存中的形式是这样的:切片首位元素地址引用自数组元素地址,切片在内存中储存着从数组中引用的元素地址,元素长度,元素容量,同数组取元素,切片也是根据其存储的切片长度通过首位元素地址进行元素取值
切片的三种定义使用方式:
- 方式一如上所示,定义一个切片让这个切片去引用已经创建好的数组
- 方式二:通过make来创建切片,基本语法:
var 切片名 []type = make([]type,len,[cap])
容量可以选填 - 方式二定义切片容量必须大于等于切片长度
- 方式三:定义一个切片,直接指定具体数组,原理类似 make 语法:
var 切片名 []type = []type {元素}
例:var a []int = []int {1,2,3}
方式二程序示例:
// 演示第二种切片使用方式
package main
import "fmt"
func main(){
var slice []int = make([]int,5,10)
slice[0] = 10
fmt.Println(slice)
fmt.Println("slice的长度为: ",len(slice))
fmt.Println("slice的容量为: ",cap(slice))
}
通过make方式创建切片,在内存中的形式如下图所示:
对上面程序总结:
- 通过make方式创建切片可以指定切片的大小和容量
- 如果没有给切片的各个元素赋值,就会使用默认值
[int,float=>0 string=>"" bool=>false]
- 通过make方式创建的切片对应的数组由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素
方式三程序示例:
// 演示第三种切片使用方式
package main
import "fmt"
func main(){
// 方式三
var slice03 []int = []int {1,2,3}
fmt.Println(slice03)
fmt.Println("slice03长度为: ",len(slice03))
fmt.Println("slice03容量: ",cap(slice03))
}
方式一和方式二的区别:
- 方式一是直接引用数组,这个数组是提前定义好的
- 方式二是通过make创建一个切片,make也会创建一个数组,这个数组没有名字,是由 make 在底层进行维护,对程序员不可见,切片第一个元素指向make创建数组的第一个元素,make创建切片示意图:
切片的遍历:
程序示例:
- 方式一:常规for-i-in进行遍历
- 方式二:for-range进行遍历
// 演示切片的遍历
package main
import "fmt"
func main(){
// 使用第三种方式创建一个切片
var sliceThree []int = []int {1,2,3,4,5}
// 第一种方式遍历切片
for i:=0;i<len(sliceThree);i++{
fmt.Println(sliceThree[i])
}
fmt.Println()
// 第二种方式遍历切片 for-range
for key,value := range sliceThree{
fmt.Printf("下标为: %v 值为: %v \n",key,value)
}
}
切片注意事项及细节说明:
-
切片是引用数据类型,进行值传递时遵循引用传递机制
程序示例: -
可以通过对数组/引用切片/二层切片元素进行地址打印可以看出来,元素引用自同一个数组
append内置函数可以对切片进行动态扩容
切片append操作的底层原理分析:
apend对数组进行扩容原理步骤:1.append过程中go底层会创建一个新的数组,这个数组长度取决于原切片长度及追加元素长度总和,新数组初始元素都置为0,然后将切片引用的数组元素拷贝进新数组,再把追加的元素拷贝进去,完成追加,等于新创建了一个数组,切片将重新引用到这个新数组,老数组将被GC
- 切片 append 操作的本质就是对数组扩容
- go底层会创建一个新的数组 newArr(安装扩容后大小)
- 将 slice 原来包含的元素拷贝到新的数组 newArr
- slice 重新引用到新创建的数组 newArr
- newArr 是在底层维护的,对程序员不可见
程序示例:
// 使用append可以对切片进行动态追加元素
package main
import "fmt"
func main(){
var sliceThree []int = []int{1,2,3,4,5}
// 通过append给切片追加元素
sliceThree = append(sliceThree,400,500,600)
fmt.Println(sliceThree)
// 通过append给切片追加切片(注:必须是切片不能是数组,结尾要加上 ... )
sliceThree = append(sliceThree,sliceThree...)
fmt.Println(sliceThree)
}
切片的拷贝操作:
- 拷贝双方数据类型必须都为slice
- sliceTwo和sliceThree的数据空间是相互独立的,拷贝完成后修改双方元素不会互相影响
- 右边切片元素拷贝到左边切片
- 拷贝可以动态兼容,不会因为拷贝双方切片元素大小差异报错
程序示例:
// 演示slice的拷贝
package main
import "fmt"
func main(){
var sliceThree []int = []int{1,2,3,4,5}
var sliceTwo []int = make([]int,10,20)
// 将 sliceThree 拷贝到 sliceTwo,注意必须都为 slice 类型
copy(sliceTwo,sliceThree)
fmt.Println(sliceTwo)
fmt.Println(sliceThree)
}
string 和 slice
- string底层是一个 byte 数组,这个byte数组实际上也是一个切片,因此string也可以进行切片处理
- string本身是不可变的,不能通过
str[0]="s"
进行元素修改 - 如果需要修改字符串元素,可以先把字符串转为byte切片或者rune切片,修改完毕重新转为string类型
- rune按字符处理,兼容汉字
string修改元素程序示例:
以上方法不能修改字符元素为汉字,因为一个汉字四个字节,如果要修改为汉字可以把字符串转为 rune切片
string进行切片处理程序示例:
// 演示 string 进行切片处理
package main
import "fmt"
func main(){
var str string = "helloworld"
slice := str[3:]
fmt.Println(slice)
}
切片练习题:
定义一个函数,传入一个int类型参数则可以求出该参数长度的斐波那契数列,并存入数组中:
// 使用数组存斐波那契数列,uint64用来存最大int数
// 创建一个函数,添加一个参数用来存数列个数,返回一个uint64切片
package main
import "fmt"
func slice(n int)([]uint64){
sliceTwo := make([]uint64,n,20)
sliceTwo[0] = 1
sliceTwo[1] = 1
for i:=2;i<len(sliceTwo);i++{
// sliceTwo[0] = 1,sliceTwo[1] = 1
sliceTwo[i] = sliceTwo[i-1] + sliceTwo[i-2]
}
return sliceTwo
}
func main(){
fmt.Println(slice(10))
}
排序和查找(主要针对数组)
冒泡排序:
冒泡排序程序示例:
- 这里需要注意一下定义一个指针int类型的数组写法
(arr *[5]int)
[5]元素个数要在前 - 通过传地址的方式用指针参数进行接收不用进行值拷贝效率比较高
// 验收一下冒泡排序
package main
import "fmt"
// 4.这里定义一个int变量用来接收存放临时数组元素
var tmp int
// 2.定义一个函数,参数定义为一个指针类型数组用来接收main函数地址类型参数
func fb(arr *[5]int){
fmt.Println("排序前数组:",*arr)
for i:=0;i<len(*arr);i++{
// 5.数组进行一轮排序需要 数组长度-1次,这里len(*arr)=5-1=4,四次,从0即位到3,一共四次
for i:=0;i<len(*arr)-1;i++{
// 6.1-2,2-3,3-4,4-5挨个判断,若判断双方右边大于左边的数,则把右边的数和左边数交换
if (*arr)[i] < (*arr)[i+1]{
// 7.判断为true,把左边的数存入临时变量
tmp = (*arr)[i]
// 8.把右边大数赋值给左边小数(*arr)[i] (*arr)[i+1] = (*arr)[i+1]
(*arr)[i] = (*arr)[i+1]
// 9.再把之前存入临时变量的小数赋值给交换前的大数,完成交换
(*arr)[i+1] = tmp
}
}
}
fmt.Println("排序后数组: ",*arr)
}
func main(){
// 1.定义一个数组
var arr = [...]int{23,645,3,45,64}
// 3.把这个数组以传地址的方式传入进行排序的函数
fb(&arr)
}
查找
在golang中常用的查找方式有两种:
- 顺序查找
- 二分查找(该数组的有序的)
顺序查找两种程序示例(推荐第二种):
// 演示golang中的查找
package main
import "fmt"
// 顺序查找第一种方式
func one(){
var name = [...]string{"金毛","哈士奇","腊肠犬","斑点狗","柯基"}
var names string
fmt.Println("请输入你最喜欢的犬种名字:")
fmt.Scanln(&names)
for i:=0;i<len(name);i++{
if names == name[i]{
fmt.Println("你喜欢的是:",name[i])
break
}else{
fmt.Println("抱歉没有这个选择")
break
}
}
}
// 顺序查找第二种方式
func two(){
var names string
var xb int
var name = [...]string{"拉布拉多","金毛","阿拉斯加","萨摩耶","泰迪"}
fmt.Println("请输入你喜欢的犬种:")
fmt.Scanln(&names)
for i:=0;i<len(name);i++{
if names == name[i]{
xb = i
break
}
}
if xb != 0{
fmt.Printf("你喜欢的犬种为:%v 对应的下标为 %v",names,xb)
}
}
func main(){
one()
two()
}
二分查找程序示例:
程序示例:
// 二分查找
package main
import "fmt"
var middle int
func fb(arr *[10]int,leftindex int,rightindex int,find int){
if leftindex>rightindex{
fmt.Println("找不到")
return
}
middle = (leftindex+rightindex)/2
fmt.Println("第一次递归",middle)
if (*arr)[middle]>find{
fb(arr,leftindex,middle-1,find)
}else if(*arr)[middle]<find{
fb(arr,middle+1,rightindex,find)
}else{
fmt.Printf("找到了下标为: %v 值为: %v",middle,(*arr)[middle])
}
}
func main(){
var tmp int
arr := [10]int{12,32,34,45,99,122,324,456,767,788}
fmt.Println("请输入所查找的值: ",arr)
fmt.Scanln(&tmp)
fb(&arr,0,9,tmp)
}
- 从以下的结果可以看出二分查找的规律,递归middle出现几次代表递归了几次
多维数组-二维数组
程序示例快速入门:
// 演示二维数组
package main
import "fmt"
func main(){
var twoarr [5][5]int
twoarr[1][2] = 1
for i:=0;i<5;i++{
for j:=0;j<5;j++{
fmt.Print(twoarr[i][j]," ")
}
fmt.Println()
}
}
使用方式1:先声明/定义,再赋值
语法:var 数组名 [大小][大小]类型
二维数组在内存中的布局形式:
使用方式二:直接初始化
- 声明:
var 数组名 [大小][大小]类型 = [大小][大小]类型 {{初值},{初值}}
- 赋值(int类型默认为0)
程序示例:
// 第二种使用数组方式
package main
import "fmt"
func main(){
var arrstwo [2][3]int = [2][3]int {{1,2,3},{2,3,4}}
fmt.Println(arrstwo)
}
二维数组的使用: 二维数组的遍历
- 方式一:双重for循环
程序示例:
// 遍历二维数组
package main
import "fmt"
func main(){
var arrsTwo = [2][5]int{{1,2,4,5,2},{4,5,6,6,1}}
for i:=0;i<len(arrsTwo);i++{
for j:=0;j<len(arrsTwo[i]);j++{
fmt.Print(arrsTwo[i][j]," ")
}
fmt.Println()
}
}
- 方式二:for-range
// 遍历二维数组
package main
import "fmt"
func main(){
var arrsTwo = [2][5]int{{1,2,4,5,2},{4,5,6,6,1}}
for index,i := range arrsTwo{
for indextwo,itwo := range i{
fmt.Printf("arrTwo[%v][%v] = %v ",index,indextwo,itwo)
}
fmt.Println()
}
}
map
map是key-value数据结构,又称为字段或关联数组,类似其他编程语言的集合
基本语法:
var map变量名 map[keytype]valuetype
使用介绍:
golang中的map的key可以是很多种类型,比如bool,数字,string,指针,channel,还可以是只包含前面几种类型的:接口,数组,结构体 通常为 int,string
注意:
slice,map,function不可以,因为这几种没法用 == 来判断
map声明示例:
注意:
声明是不会分配内存的,初始化需要 make 分配内存后才能赋值使用
示例演示:
使用说明:
map用前一定要make
map的key是不能重复的,如果重复了则以最后一个key-value为准
map的key-value是无序的
map的使用方式(推荐第二种):
- 方式一:先声明再make赋值
- 方式二:声明直接make等价于第一种
- 方式三:声明直接赋值
案例演示:
// 演示map使用案例
package main
import "fmt"
func main(){
a := make(map[string]map[string]string,2)
a["student"] = make(map[string]string)
a["student"]["name"] = "小明"
a["student"]["sex"] = "男"
a["student"]["address"] = "墨西哥"
a["studenttwo"] = make(map[string]string)
a["studenttwo"]["name"] = "小花"
a["studenttwo"]["sex"] = "18"
a["studenttwo"]["address"] = "国美在线"
fmt.Println(a["student"])
fmt.Println(a["studenttwo"])
}
map的增删改查操作:
- map的增加和更新:
如果key没有则增加,如果已有该key则为更新
- map的删除
delete(map名称,"key")
:
map删除细节说明:
- 如果要删除map所有的key,可以对map的key进行遍历删除
- 或者 map = make(…) make一个新的重新分配内存,让原来的key-value成为垃圾被GC
map查找:
如果a中存在 age 这个key,则boole返回true,values保存这个key对应的value
map的遍历
注意:map遍历一定要用for-range进行遍历,因为map映射类型不一定是数字,即使是数字也不一定是连续的数字,使用for-range直接进行元素遍历,for-in进行遍历是通过依次通过元素的位进行取的
map的长度
len(map)
map的切片:
基本介绍:
如果切片的基本数据类型为map,我们称为map切片,这样使用则map个数就可以动态变化了
// 练习slice map
package main
import "fmt"
func main(){
// 声明一个map切片
var slicemap []map[string]string
// make这个map切片,make完就可以使用了
slicemap = make([]map[string]string,2)
// 切片元素还需要make一下才能使用
slicemap[0] = make(map[string]string,2)
slicemap[0]["name"] = "猪八戒"
slicemap[0]["score"] = "哈士奇"
slicemap[1] = make(map[string]string,2)
slicemap[1]["name"] = "孙悟空"
slicemap[1]["score"] = "牛魔"
fmt.Println(slicemap)
}
map排序:
golang的map默认是无序的,每次遍历输出的map元素没有固定位置
排序思路:
注意:key必须是int类型才能进行排序
把map的key存入一个切片进行排序然后进行map根据key取值
sort.Ints可以进行排序
// 演示map排序
package main
import (
"fmt"
"sort"
)
func main(){
var slice []int
map2 := make(map[int]string)
map2[2] = "孙悟空"
map2[5] = "猪八戒"
map2[9] = "唐三藏"
map2[1] = "沙和尚"
// 对map进行遍历把key取出来追加到slice中
for k,_ := range map2{
slice = append(slice,k)
}
// 将切片排序
sort.Ints(slice)
// 对切片进行遍历,然后通过key大小对map进行顺序取值
for _,v := range slice{
fmt.Println(map2[v])
}
}
map使用细节:
- map是引用类型,遵守引用类型传递的机制,在一个函数接收map后进行修改会修改引用源map值
- map的容量达到阈值后会自动进行扩容,不会发生panic,也就是说map能动态增长键值对
- map的value也经常使用 struct (结构体) 类型,更适合管理复杂的数据
- 如果map的value还是一个map键值对使用时需要make两次
struce作为map的value程序示例:
// 演示map的value为struct类型
package main
import "fmt"
func main(){
// 声明一个struct
type stu struct{
name string
age int
address string
}
// 声明初始化一个 map,value为刚声明的stu
student := make(map[string]stu)
// struct的使用
stu1 := stu{"小明",18,"邯郸"}
stu2 := stu{"小王",20,"围魏救赵"}
// 把结构体值赋值给mapkey作为value
student["top1"] = stu1
student["top2"] = stu2
fmt.Println(student)
// 这里注意一下mapvalue的属性取值方法value.属性
for k,v := range student{
fmt.Println("学生编号为: ",k)
fmt.Println("学生名字为: ",v.name)
fmt.Println("学生年龄为: ",v.age)
fmt.Println("学生地址为: ",v.address)
}
}
map习题:
- 使用 map[string] map[string]string 类型
- key表示用户名是唯一的
- 如果某个用户名存在就将其密码修改为 8888,如果不存在就添加这个用户,及密码
package main
import "fmt"
func fb(maps map[string]map[string]string,id string){
if maps[id] != nil{
maps[id]["password"] = "888888"
}else{
maps[id] = make(map[string]string)
maps[id]["username"] = "孙悟空"
maps[id]["password"] = "888888"
}
fmt.Println(maps)
}
func main(){
maps := make(map[string]map[string]string)
maps["top1"] = make(map[string]string)
maps["top1"]["username"] = "牛魔王"
maps["top1"]["password"] = "123456"
id := "top2"
fb(maps,id)
}
map综合应用实例:
声明一个函数用来接收 map[string]map[string]string 类型的map值,进行判断知否有指定的map类型key有就对密码进行修改,没有则创建
package main
import "fmt"
func fb(map2 map[string]map[string]string,id string){
if map2[id] != nil{
map2[id]["password"] = "888888"
}else {
// 如果没有这个map则需要make一个
map2[id] = make(map[string]string)
map2[id]["password"] = "888888"
}
fmt.Println(map2)
}
func main(){
map2 := make(map[string]map[string]string)
map2["id"] = make(map[string]string)
map2["id"]["name"] = "牛魔王"
map2["id"]["password"] = "123456"
id := "孙悟空"
fb(map2,id)
}
go面向对象
struct的优点:
- 利于数据管理,不同类型的数据集中管理,方便整洁
- 使用变量定义,array,slice,map都不能进行统一的不同类型管理
- struct是值类型,结构体变量地址直接指向结构体
struct:
- 一个程序就是一个世界,有很多对象(变量)
go语言面向对象编程说明:
-
go语言支持面向对对象编程(OOP)但是和传统的面向对象编程有区别,并不是纯粹的面向对象编程,所以说golang支持面向对象编程特性是比较准确的,golang面向对象编程不够纯粹
-
golang没有类(class),golang使用struct来实现类似class的作用,可以理解为golang是基于struct来实现OOP特性的
-
golang面对对象编程非常简洁,去掉了传统的OOP语言的继承,方法重载,构造函数,析构函数,隐藏和this指针等
-
golang仍然有面向对象的继承,封装,多态的特性,只是实现方式和其他OOP语言不同,比如继承golang没有 extends 关键字,继承是通过匿名字段实现的
-
golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)进行关联,耦合性低加粗样式也非常灵活,golang中面向接口编程是非常重要的特性
对上图说明:
- 将一类事物的特征提取出来(比如猫类),形成一个新的数据类型,就是一个结构体
- 通过这个结构体,我们可以创建多个变量(实例/对象)
- 事物可以猫类,也可以Person,Fish或者是某个工具类
快速入门实例:
// struct结构体
package main
import "fmt"
func main(){
// 创建一个结构体 Cat
type Cat struct{
name string
age int
address string
}
// 创建一个Cat类型的变量
// struct是值类型,声明完变量就分配空间
var Cat1 Cat
// 给变量赋值
Cat1.name = "小黄"
Cat1.age = 10
Cat1.address = "贝吉塔星球"
fmt.Println("猫咪的名字叫: ",Cat1.name)
fmt.Println("猫咪的年龄为: ",Cat1.age)
fmt.Println("猫咪的地址: ",Cat1.address)
}
结构体和结构体实例变量的区别和练习:
- 结构体是自定义的数据类型,代表一类事物
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
结构体变量在内存中的布局:
结构体基本语法:
实例:
package main
import "fmt"
func main(){
type Cat struct{
name string
age int
address string
}
}
字段/属性:
- 从概念或叫法上看:结构体字段 = 属性 = field (统一叫字段)
- 字段是结构体的基本组成部分,一般是基本数据类型,array,也可以是引用类型
注意事项和细节说明:
- 字段声明语法同变量。实例:字段名 字段类型
- 字段的类型可以为:基本数据类型,数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个默认值。布尔类型默认值为 false,数值为 0,string为 “”,array类型的默认值和他的元素类型相关,比如 score [3]int 则为[0,0,0],Pointer和slice和map都为nil,即还未分配空间
- slice和map类型要先make才能使用
- 不同结构体变量的字段是独立的,互不影响,一个结构体遍历字段的更改,不影响另外一个,结构体是值类型结构体变量也是值类型,默认使用值拷贝,修改一个结构体变量不会影响别的结构体变量
- 结构体是值类型
创建结构体实例的四种方法:
方式一:
方式二注意事项:
- 声明定义可以直接赋值
方式三注意事项:
- 结构体变量是指针
(*p3).name = "贝吉塔星球" // 等价于 p3.name = "贝吉塔星球"
- 标准的赋值方式为
(*p3).name = "贝吉塔星球"
方式四注意事项:
- 如果想要通过一个变量修改结构体变量可以通过穿内存地址的方式
- 结构体变量是指针
- 因为是指针类型输出时候格式为:
*p4
- 声明定义可以直接赋值
struct的内存分配机制:
- 所有变量值都需要加载到内存中才能允许
重要小细节:
结构体使用注意事项和细节:
- 结构体中字段元素在内存中都是连续分布的
这里需要注意一下struct中以struct作为字段类型的使用方式
16进制,满16位进1,间隔几位取决于字段类型长度,int类型8位就间隔8位
细节二:
struct是用户自主定义的数据类型,和其它struct类型变量进行转换时需要有完全相同的字段(名字,个数和类型)
细节三:
struct类型type重新定义(相当于起别名),golang认为是新的数据类型,但是相互可以转换
序列化struct
使用场景:当客户端像服务端请求参数,则可以将struct序列化为json格式
// 演示把struct字段转换为json格式
package main
import (
"fmt"
"encoding/json"
)
type Contents struct{
// `json:username`相当于给字段名打一个tag标签
Username string `json:"username"`
Password string `json:"password"`
}
func main(){
index := Contents{"admin牛牛牛","123456qwer"}
js,err := json.Marshal(index)
if err != nil{
fmt.Println("struct类型用户信息序列化错误:",err)
}
fmt.Println(string(js))
}