Go语言中的复杂类型:函数、数组、切片、Map、结构体等
Go语言函数、数组、切片
Go语言中函数,数组这些都是派生类型,也可以说是复杂类型,能够处理更加复杂的数据。
Go语言中函数,数组这些都是派生类型,也可以说是复杂类型,能够处理更加复杂的数据。
一、函数
函数是基本的代码块,用于执行一个任务。
Go 语言最少有个 main() 函数。
函数声明告诉了编译器函数的名称,返回类型,和参数。
Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
函数声明
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
func function_name( [parameter list] ) [return_types] {
函数体
}
package main
import "fmt"
func main() {
fmt.Printf("函数类型为%T
",sum)
fmt.Printf("函数值为%d
",sum(1,2))
fmt.Printf("函数地址为%p",sum)
display()
}
func display() {
fmt.Println("这是一个无参数,也无返回值的函数")
}
func sum(a ,b int) int{
s:=a+b
fmt.Println("这是一个含有两个参数,返回一个int值的函数")
return s
}
结果为:
函数类型为func(int, int) int
这是一个含有两个参数,返回一个int值的函数
函数值为3
函数地址为0x498990
这是一个无参数,也无返回值的函数
二、数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在GO中很少直接使用数组。
数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。
数组声明
var name [SIZE] type
var arr[4] string//名为arr的长度为4的string型数组
初始化
var name [SIZE] type=[SIZE] type{...}
var s =[7]string{"Hello"," World"," Can ","you ","help ","me " ," ?"}
访问元素
直接通过下标访问或者for,range遍历访问
//k为索引,v为元素值
for k,v:=range s{}
package main
import "fmt"
func main() {
var s =[7]string{"Hello"," World"," Can ","you ","help ","me " ," ?"}
//这里的[]里面如果不填,相当于另一种类型-----切片
//s:= [7]string{"Hello"," World"," Can ","you ","help ","me " ," ?"}
//var s[7]string
// s= [7]string{"Hello"," World"," Can ","you ","help ","me " ," ?"}
fmt.Printf("数组类型为%T
",s)
fmt.Printf("数组地址为%p
",s)
fmt.Printf("数组长度为%d
",len(s))
fmt.Printf("数组第一位长度为%d
",len(s[0]))
fmt.Println("数组为",s)
fmt.Println("循环访问数组:")
for i:=0;i<len(s);i++{
fmt.Print(" "+s[i])
}
fmt.Println("
range循环访问数组元素及其下标:")
for k,v:=range s{
fmt.Print(" K: ",k)
fmt.Print(" V: ",v)
}
}
数组类型为[7]string
数组地址为%!p([7]string=[Hello World Can you help me ?])
数组长度为7
数组第一位长度为5
数组为 [Hello World Can you help me ?]
循环访问数组:
Hello World Can you help me ?
range循环访问数组元素及其下标:
K: 0 V: Hello K: 1 V: World K: 2 V: Can K: 3 V: you K: 4 V: help K: 5 V: me K: 6 V: ?
多维数组
数组可以有多维数组,如下二维数组
package main
import "fmt"
func main() {
var arr [3] [2]int= [3][2]int{{1,2},{2,3},{3,5}}
for _,v:=range arr{
fmt.Println("v的值为 ",v)
for _,s:=range v{
fmt.Print(" s的值为:",s)
}
fmt.Println()
}
}
v的值为 [1 2]
s的值为:1 s的值为:2
v的值为 [2 3]
s的值为:2 s的值为:3
v的值为 [3 5]
s的值为:3 s的值为:5
三、切片
Go 语言切片是对数组的抽象。能增长的数组。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
声明及初始化
基本等同数组,只是不需要声明长度
var s[]string
s= []string{"Hello"," World"," Can ","you ","help ","me " ," ?"}
或者使用make函数make([]T, length, capacity)
,len是初始长度,capacity可选,指容量(容量不代表最大长度,只是声明一下)
s:=make([]string,3,10)
访问
和数组一样,使用下标访问,也可以使用切片截取[start:end]
s:=[]string{"Hi ","My ","Friend ","!"}
fmt.Println("数组:",s)
//访问元素,slice [开始位置:结束位置]
fmt.Println("切片:",s[0:2])
//访问元素,类似数组
fmt.Println("切片:",s[0])
数组: [Hi My Friend !]
切片: [Hi My ]
切片: Hi
append()元素追加,cap()计算容量
追加新元素使用 append 方法,追加元素,非原地操作,会生成新切片
package main
import "fmt"
func main() {
s:=[]string{"Hi ","My ","Friend ","!"}
fmt.Println("数组:",s)
//访问元素,slice [开始位置:结束位置]
fmt.Println("切片:",s[0:2])
//访问元素,类似数组
fmt.Println("切片:",s[0])
fmt.Println("切片后:",s)
fmt.Println("for---range遍历数组")
for _,v:=range s{
fmt.Print(" ",v)
}
fmt.Println("此时切片容量为",cap(s))
//追加元素,非原地操作,会生成新切片
fmt.Println("append方法追加元素: ",append(s, " yes"))
//追加多个元素
fmt.Println("append方法追加元素: ",append(s, " it's ","is ","good!"))
fmt.Println("此时切片容量为",cap(s))
n:=append(s, " it's ","is ","good!")
//当 s追加元素时,append(numbers, 2, 3, 4) 为什么 cap 从 4 变成 8 ?
//
//经过实践得知,append(list, [params]),先判断 list 的 cap 长度是否大于等于 len(list) + len([params]),
// 如果大于那么 cap 不变,否则 cap 等于 2*max{cap(list), cap[params]},所以当 append(s, 2, 3, 4) cap 从 2 变成 6。
fmt.Printf("此时切片容量为%d
",cap(n))
fmt.Println("值是:",n)
}
数组: [Hi My Friend !]
切片: [Hi My ]
切片: Hi
切片后: [Hi My Friend !]
for---range遍历数组
Hi My Friend !此时切片容量为 4
append方法追加元素: [Hi My Friend ! yes]
append方法追加元素: [Hi My Friend ! it's is good!]
此时切片容量为 4
此时切片容量为8
值是: [Hi My Friend ! it's is good!]
切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……,代码如下:
var numbers []intfor i := 0; i < 10; i++ {
numbers = append(numbers, i)
fmt.Printf("len: %d cap: %d pointer: %p
", len(numbers), cap(numbers), numbers)}
代码输出如下:
len: 1 cap: 1 pointer: 0xc0420080e8
len: 2 cap: 2 pointer: 0xc042008150
len: 3 cap: 4 pointer: 0xc04200e320
len: 4 cap: 4 pointer: 0xc04200e320
len: 5 cap: 8 pointer: 0xc04200c200
len: 6 cap: 8 pointer: 0xc04200c200
len: 7 cap: 8 pointer: 0xc04200c200
len: 8 cap: 8 pointer: 0xc04200c200
len: 9 cap: 16 pointer: 0xc042074000
len: 10 cap: 16 pointer: 0xc042074000
代码说明如下:
- 第 1 行,声明一个整型切片。
- 第 4 行,循环向 numbers 切片中添加 10 个数。
- 第 5 行,打印输出切片的长度、容量和指针变化,使用函数 len() 查看切片拥有的元素个数,使用函数 cap() 查看切片的容量情况。
往一个切片中不断添加元素的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。
通过查看代码输出,可以发现一个有意思的规律:切片长度 len 并不等于切片的容量 cap。
- 员工和工位就是切片中的元素。
- 办公地就是分配好的内存。
- 搬家就是重新分配内存。
- 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
- 由于搬家后地址发生变化,因此内存“地址”也会有修改。
Go语言指针及结构体
指针是一种存储地址的数据类型,引用类型;结构体是一种能够组合各种数据类型的类型,值类型。
指针是一种存储地址的数据类型,引用类型;结构体是一种能够组合各种数据类型的类型,值类型。
指针是存储地址的类型,c/c++里面的指针极度强大,通过对指针的偏移、运算和转换 ,是C/C++ 语言拥有极高性能的根本所在,在操作大块数据和做偏移时即方便又便捷,但是C/C++ 中指针饱受诟病,根本原因是指针的运算和内存释放,经常造成内存溢出等问题。
Go语言也提供了指针,但是不允许指针偏移和运算,这样既安全又保证了操作数据的便利。
一、指针
指针是一种存储地址的数据类型,引用类型
变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址
声明及初始化
指针声明通过*,取地址通过&,也可以使用new函数获取一个类型的指针(已初始化)
//格式
var pointerName *type
var x int=10
//声明为int型的指针
var num *int
//初始化,取到x的地址
num=&x
//new函数创建指针
var str *string=new(string)
指针的值、指针地址、通过指针访问数据
指针是存储地址的引用类型,所以指针的值就是地址,指针也有自己的地址,指针存储的地址也有对应的数据,可以通过指针访问到。
demo1:通过数组指针来说明
var str [4]string=[4]string{"Hello","Good","Yes","Nice"}
//数组指针
var p =&str
fmt.Println("指针中存储的是地址:",p)
fmt.Println("指针本身的地址:",&p)
fmt.Println("指针中存储的地址的值:",*p)
fmt.Println("数组指针访问数组,通过下标",p[0])
//修改数据
p[1]="Bad"
fmt.Println("指针是引用类型,所以原数据也被修改:",*p)
指针中存储的是地址: &[Hello Good Yes Nice]
指针本身的地址: 0xc00008c018
指针中存储的地址的值: [Hello Good Yes Nice]
数组指针访问数组,通过下标 Hello
指针是引用类型,所以原数据也被修改: [Hello Bad Yes Nice]
demo2:还是数组指针
s:=[5] int{1,3,1,4,77}
//ptr:=&s
var ptr *[5]int
ptr=&s
fmt.Printf("数组的类型%T
",s)
fmt.Printf("指针的类型%T
",ptr)
for _,v:= range ptr{
fmt.Printf("数据为:%d
",v)
}
i:= new([5] string)
fmt.Printf("类型是%T
",i)
l:=[5]string{"Hello","yes","good","nice","well"}
i=&l
fmt.Printf("类型是%T
",i)
数组的类型[5]int
指针的类型*[5]int
数据为:1
数据为:3
数据为:1
数据为:4
数据为:77
类型是*[5]string
类型是*[5]string
demo3:通过结构体指针来说明,和以上一样,值得一提的的是访问结构体数据,可以省略*。
关于结构体,见下文。
func main(){
booKs := BooKs{"《西游记》", "吴承恩"}
ptr := new(BooKs)
ptr=&booKs
//结构体指针访问数据,两种都可以
fmt.Println((*ptr).name,ptr.author)
}
type BooKs struct {
name string
author string
}
《西游记》 吴承恩
数组指针、指针数组
数组指针是数组类型的指针
var apt [6]int//int型的数组
var aptr *[6] int//int型数组指针
var aptr *[6] int
var arr [6] int=[6] int{1,2,3,4,5,6}
aptr=&arr
fmt.Println(*aptr)
//[1 2 3 4 5 6]
指针数组是数组,其中的数据类型是地址
a:="hello"
b:="yes"
c:="world"
var parr [3]*string=[3]*string {&a,&b,&c}
fmt.Println("指针数组中存储的是地址:",parr)
fmt.Println("通过下标来访问数据:",parr[0])
fmt.Println("取得地址所对应的数据",*parr[0])
//*parr非法,因为
指针数组中存储的是地址: [0xc0000301f0 0xc000030200 0xc000030210]
通过下标来访问数据: 0xc0000301f0
取得地址所对应的数据 hello
二、结构体
结构体是类型中带有成员的复合类型,值类型。
Go 语言不是面向对象的语言,但可以使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。
结构体(struct)和其他语言的类(class)有同等的地位,但Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。
结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:字段拥有自己的类型和值。字段名必须唯一。字段的类型也可以是结构体,甚至是字段所在结构体的类型。
type关键字
关键字 type 可以将各种基本类型定义为自定义类型,结构体、接口等使用type关键字来声明。
结构体的定义格式如下:
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
对各个部分的说明:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- struct{}:表示结构体类型,type 类型名 struct{} 可以理解为将 struct{} 结构体定义为类型名的类型。
- 字段1、字段2……:表示结构体字段名。结构体中的字段名必须唯一。
- 字段1类型、字段2类型……:表示结构体字段的类型。
如下:
type Person struct {
name string
age int
gender string
}
结构体中还可以嵌套结构体:
type Person struct {
name string
age int
gender string
}
type Book struct {
name string
anthor string
}
type Read struct {
person Person
book Book
}
声明及初始化
结构体初始化有多种方式
//初始化,不带字段
var p=Person{"Tom",22,"female"}
//初始化,带字段
var p1=Person{name:"Tom",age:22,gender:"female"}
//只做声明
var p2 Person
//赋值
p2.name="Jack"
p2.age=19
p2.gender="male"
fmt.Println("P:",p)
fmt.Println("P1:",p1)
fmt.Println("P2:",p2)
//嵌套结构体的初始化
var read=Read{p,Book{"《西游记》","吴承恩"}}
fmt.Printf("%v
",read)
//访问嵌套的数据
fmt.Println(read.book)
fmt.Println(read.book.name)
}
type Person struct {
name string
age int
gender string
}
type Book struct {
name string
anthor string
}
type Read struct {
person Person
book Book
}
结果如下:
P: {Tom 22 female}
P1: {Tom 22 female}
P2: {Jack 19 male}
{{Tom 22 female} {《西游记》 吴承恩}}
{《西游记》 吴承恩}
《西游记》
new关键字初始化
new关键字是用来初始化的,返回的是指向类型的指针,关于new的用法在下一篇也有讲解,在此只需要知道是用来返回指针即可。
new关键字的用法是
v:=new(type)
用new关键字创建的都是指针,创建结构体就是结构体指针。如下:
func main() {
p:=Persons{"Sally",18,"female"}
//new关键字得到的是结构体类型的指针
p1:=new(Persons)
//通过取地址符拿到数据
p1=&Persons{"Jim",10,"male"}
fmt.Printf("p的类型是%T
",p)
fmt.Printf("p1的类型是%T
",p1)
fmt.Printf("p的名字是%s,年龄为%d
",p.name,p.age)
//两种取数据方式都可以
fmt.Printf("p1的名字是%s,年龄为%d
",(*p1).name,p1.age)
}
type Persons struct {
name string
age int
gender string
}
p的类型是main.Persons
p1的类型是*main.Persons
p的名字是Sally,年龄为18
p1的名字是Jim,年龄为10
p1是结构体类型的指针,p是结构体。
指针和结构体非常强大,指针可以用来操作地址,结构体则可以完成各种类型的组合,还可以模拟面向对象的各种特性。
Go语言map、list及关键字make&new
map和列表是两种容器,make&new是创建以及初始化结构的
map和列表是两种容器,make&new是创建以及初始化结构的
一、make&new关键字
make和new关键字是用来创建以及初始化结构的。
make只用于chan、map以及切片的内存创建,返回的还是这三个引用类型本身;
因为这三种类型是引用类型,所以必须得初始化。对于引用类型的变量,我们不光要声明它,还要为它分配内容空间。
make和new关键字都分配内存。
而new返回的是指向类型的指针。
func main() {
//make创建切片,已被初始化
slice := make([] int, 3)
slice[0]=1
slice[1]=2
fmt.Printf("slice的类型是%T
",slice)
//make创建map,已被初始化
maps:=make(map[int] int)
maps[0]=2 //已被初始化的map可以存放数据
fmt.Printf("maps的类型是%T
",maps)
//返回一个指针
l:=new(string)
var n string
n="hahah"
l=&n
fmt.Printf("l的类型是%T,地址是%p,值是%v",l,&l,*l)
}
slice的类型是[]int
maps的类型是map[int]int
l的类型是*string,地址是0xc000006030,值是hahah
二、map
map是一种数据结构,采用键值对的存储格式,是一种无序的数据结构,引用类型的容器,在其他语言中也叫字典,hash等。
键:值,键和值分别有自己的类型
声明及初始化
//创建map的三种方式
var map1 map[int]string//只做声明,未被初始化,不能存放数据
var map2 map[int]string=map[int]string{1:"hello",2:"yes",4:"my",3:"王二狗",9:"隔壁老王"}
//key和value都是string
map3:=make(map[string]string)
增删改查
map通过键来访问数据,增加数据,修改数据,使用delete()来删除数据。
//访问和改变都通过key改变
map3["one"]="Hi"
map3["two"]="boy"
//删除元素[map,key]
delete(map2,3)
delete(map2,1)
遍历
遍历使用for和range关键字,其中返回的是键和值
for k,v:=range map{
}
具体实例见下
package main
import "fmt"
func main() {
//创建map的三种方式
var map1 map[int]string//只做声明,未被初始化,不能存放数据
var map2 map[int]string=map[int]string{1:"hello",2:"yes",4:"my",3:"王二狗",9:"隔壁老王"}
//key和value都是string
map3:=make(map[string]string)
fmt.Printf("map1的类型是%T,值是%v
",map1,map1)
fmt.Printf("map2的类型是%T,值是%v
",map2,map2)
fmt.Printf("map3的类型是%T,值是%v
",map3,map3)
fmt.Println("----------------------")
map1=map2
fmt.Println("map1[1] : "+map1[1])
//访问和改变都通过key改变
map3["one"]="Hi"
map3["two"]="boy"
fmt.Println("map3的值: ",map3)
fmt.Println("----------------------")
//遍历键值
for k,v:=range map2{
fmt.Printf("key为%v,值为%v
",k,v)
}
fmt.Println("----------------------")
//删除元素[map,key]
delete(map2,3)
delete(map2,1)
fmt.Println("----------------------")
fmt.Println("遍历map2")
for k,v:=range map2{
fmt.Printf("key为%v,值为%v
",k,v)
}
fmt.Println("..............................")
fmt.Println("遍历map1")
//引用类型,原数据也被修改了
for k,v:=range map1{
fmt.Printf("key为%v,值为%v
",k,v)
}
}
map1的类型是map[int]string,值是map[]
map2的类型是map[int]string,值是map[1:hello 2:yes 3:王二狗 4:my 9:隔壁老王]
map3的类型是map[string]string,值是map[]
----------------------
map1[1] : hello
map3的值: map[one:Hi two:boy]
----------------------
key为9,值为隔壁老王
key为1,值为hello
key为2,值为yes
key为4,值为my
key为3,值为王二狗
----------------------
----------------------
遍历map2
key为9,值为隔壁老王
key为2,值为yes
key为4,值为my
..............................
遍历map1
key为2,值为yes
key为4,值为my
key为9,值为隔壁老王
三、列表
列表不是go内置的数据结构,来自"container/list"包下,底层基于双链表。
列表与切片和 map 不同的是,列表并没有具体元素类型的限制。因此,列表的元素可以是任意类型。这既带来遍历,也会引来一些问题。给一个列表放入了非期望类型的值,在取出值后,将 interface{} 转换为期望类型时将会发生宕机。
声明及初始化
- 通过 container/list 包的 New 方法初始化 list
变量名 := list.New()
- 通过声明初始化list
var 变量名 list.List
列表操作涉及到方法,可以查看文档。
package main
import (
"container/list"
"fmt"
)
func main() {
//初始化
var l =list.New()
var h list.List
//从后加入元素
h.PushBack("hahhahhhh")
l.PushBack("Hello")
l.PushFront("你好")
l.PushFront(111)
//取值
fmt.Println(l.Front().Value)
fmt.Println(l.Back().Value)
//获取长度
fmt.Println(l.Len())
p:=Animal{"Jack",19}
//加入结构体
l.PushBack(p)
fmt.Println("最后一位元素的值是",l.Back().Value)
//返回元素的指针
ptr:=l.PushBack("NO")
fmt.Println(ptr)
//插入到指定元素之前
l.InsertBefore("nice",ptr)
//遍历
for i:=l.Front();i!=nil;i=i.Next(){
fmt.Println(i.Value)
}
//删除元素,通过指针标记
l.Remove(ptr)
fmt.Println("...........................")
//再遍历
for i:=l.Back();i!=nil;i=i.Prev(){
fmt.Println(i.Value)
}
}
type Animal struct {
name string
age int
}
111
Hello
3
最后一位元素的值是 {Jack 19}
&{0xc00005c330 0xc00005c450 0xc00005c330 NO}
111
你好
Hello
{Jack 19}
nice
NO
...........................
nice
{Jack 19}
Hello
你好
111