数组
数组是一种数据类型元素的集合。在Go语言中,数组在声明时就必须确定它的数据类型以及长度,虽然数组在使用时可以修改其数据,但是数组大小无法改变。
数组定义
go
var 数组变量名 [元素数量]T
数组在定义时的长度必须是常量,数组的长度也是数组类型的一部分。不同长度的数组是不同类型的。例如[10]int
和[100]int
是不同的类型。
go
var a [5]int
var b [50]int
数组初始化
数组必须经过初始化才可以使用。
go
func main() {
var arr1 [3]int //自动初始化,值为0
arr2 := [3]string{"aa","bb","cc"} //根据值进行初始化
fmt.Println(arr1) //[0, 0, 0]
fmt.Println(arr2),//[aa, bb, cc]
}
go语言中,数组的初始化我们也可以不指定长度,我们可以让编译器根据初始值自动推导出数组的长度。
go
func main() {
var arr1 [3]int
arr2 := [...]int{11, 22, 33, 44, 55}
fmt.Println(arr1) //[0, 0, 0]
fmt.Println(arr2) //[11, 22, 33, 44, 55]
fmt.Println(len(arr2)) //5
}
我们也可以根据索引值,来进行初始化,根据索引值,编译器能够根据最大索引自动推导出数组长度,并且根据初始值来进行初始化数组。
go
func main() {
arr1 := [...]int{1: 111, 3: 333, 5: 555}
fmt.Println(arr1) //[0, 111, 0, 333, 0, 555]
fmt.Println(len(arr1)) //6
}
遍历数组
数组遍历我们可以通过for i...
的方式或者for range...
两种方式进行遍历。
go
func main() {
arr := [...]string{"aaa", "bbb", "ccc", "ddd"}
//通过for i的方式进行遍历
for i:=0; i<len(arr); i++ {
fmt.Println(arr[i])
}
//aaa
//bbb
//ccc
//ddd
//通过for range的方式进行遍历
//index表示索引,value表示值
for index, value := range arr {
fmt.Println(index, value)
}
//0 aaa
//1 bbb
//2 ccc
//3 ddd
}
多维数组
go语言中支持多维数组(数组嵌套),我这就拿二维数组进行讲解。
多维数组定义
多维数组在定义上与一维数组几乎一致,只是在内层又嵌套了几层。
go
func main() {
arr1 := [3][2]string{
{"江苏", "苏州"},
{"浙江", "杭州"},
{"福建", "福州"},
}
fmt.Println(arr1) //[[江苏 苏州] [浙江 杭州] [福建 福州]]
fmt.Println(arr1[0][1]) //苏州
//多维数组也可以让编译器帮我们自动推导出数组长度,但是仅限在最外层
arr2 := [...][2]int{
{11, 111},
{22, 222},
{33, 333},
}
fmt.Println(arr2) //[[11 111] [22 222] [33 333]]
fmt.Println(len(arr2), len(arr2[0]) //3 2
}
多维数组遍历
多维数组的遍历与一维数组几乎也一致,但是我们如果需要的是内层的数据,那么我们需要由外往内逐层遍历。
go
func main() {
arr := [3][2]string{
{"江苏", "苏州"},
{"浙江", "杭州"},
{"福建", "福州"},
}
for _, row := range arr {
for _, val := range row {
fmt.Printf("%v\t", val)
}
fmt.Println()
}
//江苏 苏州
//浙江 杭州
//福建 福州
}
切片
go语言中的切片与数组看似很像,实则却不一样。
- 数组的长度是固定的,而切片长度是可变的;切片可以根据需求动态增加切片长度或者缩减切片长度。
- 数组是值类型,当进行赋值或者传递时,会复制整个数组;而切片是引用类型,在赋值或者传递时,只会复制指向切片底层数组的引用(地址)。
- 数组在声明时就会分配好固定大小的连续空间;而切片则会通过指向底层数组的指针、长度和容量来引用一段连续内存空间。
- 数组的索引是固定从0开始到长度-1;而切片可以进行多种灵活的切片操作,从而获取指定范围的子切片。
- 数组的长度也是数组类型的一部分,无法改变;而切片的长度也是动态的,我们可以通过
len()
获取并改变其长度。
当然,具体的使用还需要根据具体的场景而定。切片的灵活性使其更适合出现在动态集合的操作上,而数组更适用于大小固定的、性能要求较高的场景下。
切片定义
切片在定义时,不需要指定长度。
go
var 变量名 []变量类型
//声明切片类型
func main() {
var a []string //声明一个字符串切片
b := []int{} //声明一个整型切片并进行初始化
c := []int{11, 22, 33} //声明一个整型切片并进行初始化
d := []bool{true, false} //声明一个布尔型切片并进行初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[11 22 33]
fmt.Println(d) //[false true]
fmt.Println(a==nil) //true
fmt.Println(b==nil) //false
fmt.Println(c==nil) //false
fmt.Println(d==nil) //false
}
切片表达式
我们可以通过len()
来获取切片的长度,也可以使用cap()
来获取切片的容量。
切片表达式有两种,一种是只指定low
和high
两个索引界限值,还有一种是除了low
和high
还要指定容量。
通过上面的学习,我们了解到了切片的底层也是数组,因此我们可以通过切片表达式来得到切片。切片表达式中的low
和high
表示索引范围(左包含,右不包含)。而得到的新切片的长度=high-low
,而新切片的容量=原切片的长度-low
。
go
func main() {
slice := []int{11,22,33,44,55}
s1 := slice[2:3]
fmt.Println(s1, len(s1), cap(s1))
//[33] 1 3
s2 := slice[2:]
fmt.Println(s2, len(s2), cap(s2))
//[33 44 55] 3 3
s3 := slice[:3]
fmt.Println(s3, len(s3), cap(s3))
//[11 22 33] 3 5
s4 := slice[:]
fmt.Println(s4, len(s4), cap(s4))
//[11 22 33 44 55] 5 5
s6 := slice[1:2:3] //注意 指定的容量不能小于切片长度,也不能大于原切片的容量
fmt.Println(s6, len(s6), cap(s6))
//[22] 1 3
}
通过make()函数构造切片
以上方式我们都是基于数组来创建切片,如果我们需要动态的创建一个切片,我们需要使用make()
函数。这里我们需要注意的是,我们分配的容量可能远大于切片长度,但是这并不影响当前切片的使用。
go
make([]元素类型, 切片长度, 切片容量)
func main() {
a := make([]int, 2, 20)
fmt.Println(a, len(a), cap(a)) //[0 0] 2 20
}
切片的本质其实就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度以及切片的容量。
切片的判断
要判断切片是否为空,需要使用len(slice)==0
来进行判断。
这里我们需要知道一点,声明的切片在未进行初始化时,它为nil
,当进行初始化后,它不为nil
。同时一个nil
值的切片的长度和容量都是0,但是我们不能说一个长度和容量为0的切片一定是nil
。
切片的赋值拷贝
切片的底层是是一个指向数组的指针,所以当我们拷贝当前切片时,对拷贝后的切片进行修改,也会导致当前切片的改变。我们可以将它们理解为指向同一地址的两个指针。
go
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,使得s2和s1指向同一地址
s2[1] = 111
fmt.Println(s1) //[0 111 0]
fmt.Println(s2) //[0 111 0]
}
切片遍历
切片的遍历与数组几乎一致,支持for i...
遍历和for range...
遍历。
go
func main() {
s := []int{11,22,33}
for i:=0; i<len(s); i++ {
fmt.Println(i, s[i])
}
//1 11
//2 22
//3 33
for index, value := range s {
fmt.Println(index, value)
}
//1 11
//2 22
//3 33
}
append()切片添加元素
go语言中的append()
函数可以动态的为切片添加元素。一次可以添加一个元素,也可以添加多个元素,还可以在另一个切片中的元素(在另一个切片后加...)。
通过var
声明的零值切片可以直接在append()
函数中使用,不需要进行初始化。
go
func main() {
var s1 []int
s1 = append(s1, 11) //[11]
s1 = append(s1, 22, 33) //[11 22 33]
s2 := []int{44, 55}
s1 = append(s1, s2...) //[11 22 33 44 55]
}
这时我们就会发现一件事,当我们的切片添加到大于切片容量会怎么办?在切片底层,它会自动进行"扩容","扩容"操作一般发送在append()
函数中,所以我们需要用原变量去接收append()
的返回值。
切片中的"扩展"发生在切片底层数组长度已经到达当前切片容量时,它会自动进行扩容,在容量小于1024的情况下,每次扩容后的容量都是扩展前的容量的两倍。如果我们并未进行初始化切片的容量,那么切片的容量会按照1,2,4,8...这样的规则进行扩容。如果此时我们给切片进行初始化10的容量,当容量已满时,它会自动扩容为20,40,80...这样的规则。
go
func main() {
var s1 []int
fmt.Println(cap(s1)) //0
s1 = append(s1, 1)
fmt.Println(cap(s1)) //1
s1 = append(s1, 1)
fmt.Println(cap(s1)) //2
s1 = append(s1, 1)
fmt.Println(cap(s1)) //4
s2 := make([]int, 3)
fmt.Println(cap(s2)) //3
s2 = append(s2, 1,1,1)
fmt.Println(cap(s2)) //6
}
当容量大于等于1024的情况下,每次扩展仅会扩容原先的四分之一。例如当前容量为1024,那么扩容后的容量为1281(1024+1024*1/4)
。
copy()复制切片
由于切片是引用类型,所以当我们将原切片赋值给新切片时,它们还是指向同一个地址,当我们修改新切片时原切片也会跟着修改。
那么当我们需要用到原切片的数据,又不想改变原切片时,我们该怎么办呢?
go语言为我们提供了copy()
函数,用于将一个切片的数据复制到另一个切片中。
go
copy(数据来源切片,目标切片)
func main() {
a := []int{1,2,3,4,5}
b := make([]int, 5, 5)
copy(a, b) //将a中的数据复制到b中
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[3] = 33
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 33 4 5]
}
删除切片元素
go语言中并未提供删除切片元素的专用方法,但是我们可以通过切片本身的特性来删除元素。
go
删除切片a中索引为index的元素
append(a[:index], a[index+1]...)
func main() {
a := []int{11,22,33,44,55}
a = append(a[:2], a[3:]...)
fmt.Println(a) //[11 22 44 55]
}
map
map是一种无序的基于key-value
的数据结构,go语言中的map是引用类型,必须初始化才可以使用。
map定义
map类型的变量默认初始值为nil, 基本语法:
go
map[keyType]valueType
map使用make()
来分配内存, map的初始化:
go
make(map[keyType]valueType, [cap])
map使用
map中的数据都是成对出现。
go
func main() {
m1 := make(map[string]string, 3)
m1["江苏"] = "苏州"
m1["浙江"] = "杭州"
fmt.Println(m1)
//map[江苏:苏州 浙江:杭州]
m2 := map[string]string {
"name": "zhangsan",
"age": "18",
}
fmt.Println(m2)
//map[age:18 name:zhangsan]
}
判断键是否存在
go语言中判断键是否存在的特殊写法格式如下:
go
value, ok := map[key]
func main() {
m1 := make(map[string]string, 3)
m1["江苏"] = "苏州"
m1["浙江"] = "杭州"
v, ok := m1["江苏"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此地")
}
}
map遍历
go语言中使用for range...
遍历map。
go
func main() {
m1 := make(map[string]string, 3)
m1["江苏"] = "苏州"
m1["浙江"] = "杭州"
for k, v := range m1 {
fmt.Println(k, v)
}
//江苏 苏州
//浙江 杭州
for k := range m1 {
fmt.Println(k)
}
//江苏
//浙江
}
delete()删除键值对
go语言中使用delete()
来完成map中键值对的删除。
go
delete(map, key)
func main() {
m1 := make(map[string]string, 3)
m1["江苏"] = "苏州"
m1["浙江"] = "杭州"
fmt.Println(m1) //map[江苏:苏州 浙江:杭州]
delete(m1, "江苏")
fmt.Println(m1) //map[浙江:杭州]
}
map的嵌套
友情提示,禁止过分套娃!
go
func main() {
//当map的元素为map时
m1 := make([]map[int]int, 3)
fmt.Println(m1) //[map[] map[] map[]]
m1[0] = make(map[int]int, 2)
fmt.Println(m1) //[map[] map[] map[]]
m1[0][0] = 11
m1[0][1] = 22
fmt.Println(m1) //[map[0:11 1:22] map[] map[]]
//当map的值为切片时
m2 := make(map[int][]int, 3)
fmt.Println(m2) //map[]
m2[1] = make([]int, 5)
m2[1] = append(m2[0], 11,22,33,44,55)
fmt.Println(m2) //map[1:[11 22 33 44 55]]
}
函数
函数是一个组织好的,可复用的,用于执行指定任务的代码块。
函数定义
go语言中使用func
关键字定义函数。
go
func 函数名(参数) (返回值) {
函数体
}
func add(x int, y int) int {
return x+y
}
func sayhi(name string) {
fmt.Println("hi", name)
}
函数调用
定义了函数后,我们可以通过函数名()
进行调用函数。
go
func add(x, y int) int {
return x+y
}
func main() {
res := add(1, 2)
fmt.Println(res) //3
}
函数参数
参数简写
当函数参数的相邻变量的类型相同时,我们可以省略类型。
go
func add(x, y int) int {
return x+y
}
可变参数
当参数数量不固定时,我们可以通过在参数名后加...
来标识。这里需要注意,可变参数只能作为函数的最后一个参数。
go
func sum(x...int) int {
res := 0
for _, v := range x {
res += v
}
return res
}
func main() {
res := sum(1,2,3)
fmt.Println(res) //6
}
函数返回值
go语言中通过return
向外输出返回值。
多返回值
go语言中的函数支持多返回值,函数如果有多个返回值时需要用()
将所有返回值包裹起来。
go
func cal(x, y int) (int, int) {
sum := x+y
sub := x-y
return sum, sub
}
返回值命名
go语言中函数定义时可以给返回值命名,我们在函数体中可以直接使用这些变量,并且最后可以通过return
自动返回。
go
func cal(x, y int) (sum, sub int) {
sum := x+y
sub := x-y
return
}
变量作用域
全局变量
全局变量定义在函数外部,它在整个程序运行周期内都有效。函数中可以直接访问全局变量。
go
package main
import "fmt"
//全局变量
var a = 1
func main() {
fmt.Println(a)
}
局部变量
局部变量无法在函数外部使用,只能在当前函数内使用。当局部变量与全局变量重名时,优先使用局部变量。当变量定义在if
,for
中,那么该变量的作用域也仅限于if
和for
的语句块中。
go
package main
import "fmt"
//全局变量
var a = 1
func main() {
//局部变量
b := 2
a := 3
fmt.Println(b) //2
fmt.Println(a) //3
}
函数作为参数或返回值
函数作为参数:
go
func add(x, y int) int {
return x+y
}
func cal(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
res := cal(1,2,add)
fmt.Println(res) //3
}
函数作为返回值:
go
func cal(x, y int, s string) func(int, int) int {
switch s {
case "+" :
return add
case "-" :
return sub
default :
return nil
}
}
匿名函数和闭包
匿名函数
当函数作为返回值时,但是go语言函数内部无法像之前那样定义函数,因此只能定义匿名函数,匿名函数就是没有函数名的函数,而且会立即执行。
go
func(参数)(返回值){
函数体
}
func main() {
//将匿名函数保存到变量中
add := func(x, y int){
fmt.Println(x+y)
}
add(1,4) //5
//自动执行函数
func(x, y int) {
fmt.Println(x-y)
}(4, 1) //3
}
闭包
在go语言中,闭包是一种特殊的匿名函数,它可以访问并且操作函数体外部的变量。闭包函数包含了一个函数和一个引用环境,这个引用环境可以是函数体外部定义的变量。闭包=函数+引用环境
。
变量incr
是一个函数,并且它引用了其外部作用域中的sum
变量,此时incr
就是一个闭包。在incr
的生命周期内,变量sum
一直有效。
go
func main() {
//定义一个闭包函数
add := func() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
// 使用闭包函数
incr := add()
fmt.Println(incr(1)) //1
fmt.Println(incr(2)) //3
fmt.Println(incr(3)) //6
}
defer语句
go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理语句按defer
定义的逆序进行执行。先被defer
的语句最后执行,后被defer
的语句最先执行。
go
func main() {
defer fmt.Println(11)
defer fmt.Println(22)
defer fmt.Println(33)
//33
//22
//11
}
go语言中defer
这一延迟调用的特性,在资源释放问题上非常方便。拿线程举例,每条线程在执行完毕之后我们都需要将其释放,而有了defer
这一特性,我们可以在开启线程后就使用defer
进行释放线程,当代码全部执行完毕,线程也会跟着释放。
内置函数
内置函数 | 介绍 |
---|---|
close | 关闭channel |
len | 用来求长度 |
new | 用来分配内存。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型。 |
append | 用来追加元素到数组、slice中 |
panic/recover | 错误处理 |
panic/recover
go语言中没有异常机制,但是可以使用panic/recover
来处理错误。panic
可以在任何地方使用,但是recover
只能在defer
调用的函数中有效。
go
func func1() {
fmt.Println("func1")
}
func func2() {
defer func() {
err := recover()
//当程序中出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in func2")
}
}()
panic("panic in func2")
}
func func3() {
fmt.Println("func3")
}
func main() {
func1()
func2()
func3()
//func1
//recover in 2
//func3
}
指针
这里我们需要先了解指针的三个概念:指针地址、指针类型、指针取值。
任何数据加载进入内存后,都拥有一个它们的地址值,这就是指针。而为了保存这个数据在内存中的地址值,我们就需要指针变量。
指针地址和指针类型
每个变量在运行时都有一个地址值,这个地址代表该变量在内存中的位置。go语言中使用&
字符放在变量前对变量进行"取地址"的操作。而go语言中的值类型都有对应的指针类型,指针类型就是在值类型的前面加上*
字符,例如:*int
、*string
等。
go
指针变量 := &地址变量
func main() {
a := 1
b := &a
fmt.Printf("%v, %p\n", a, &a) //1, 0xc00000e0b8, int
fmt.Printf("%v, %p\n", b, b) //0xc0000a6058, 0xc0000ca018, *int
}
指针取值
这里我们非常容易混淆指针、地址。对变量取地址(&)可以获得这个变量的指针变量。指针变量的值就是指针地址。对指针变量取值(*)可以获得指针变量指向的原变量的值
go
func main() {
a := 1
b := &a //取a的地址,保存到b中
fmt.Printf("%T\n", b) //*int
c := *b //对指针取值
fmt.Printf("%T\n", c) //int
fmt.Printf("%v\n", c) //1
}
new
在go语言中,new函数用于内存分配,但是new出来的变量类型是指针类型,并且该变量的值为该类型的零值。
go
func new(Type) *Type
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n",a) //*int
fmt.Printf("%T\n",b) //*bool
fmt.Println(*a) //0
fmt.Println(*b) //false
//声明一个*int类型的c变量
var c *int
//进行初始化后才可以使用
c = new(int)
fmt.Println(*c) //0
}
make与new类似,两者都是用于内存分配。但是make只用于slice、map以及channel的初始化,返回类型是这三个引用类型本身。然而new用于类型的内存分配,该内存对应值为该类型的零值,返回类型是该类型的指针类型。
结构体
当我们想表达一个事物的全部或部分属性时,我们无法使用单一的数据类型来满足需求,这个时候我们需要一种自定义数据类型,来完成对该事物的封装。而这种数据类型在go中,被称为结构体,英文名称struct
。
对于学习过Java的各位小伙伴们一定知道,Java中万物皆对象,然而在go中,我们通过struct
来面向对象。
结构体的定义
我们使用type
和struct
来定义结构体。
类型名表示自定义结构体的名称,并且同一包下不能重复。字段名表示结构体的属性名,在当前结构体内字段名必须唯一。字段类型则是表示字段的具体类型。
go
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}
我们定义一个学生结构体与老师结构体:
go
type student struct {
name string
class string
age int
}
//相同类型字段可以写在同一行
type teacher struct {
name, course string
age int
}
目前,我们拥有两个自定义类型,一个学生类型和一个老师类型。学生类型拥有姓名、班级、年龄三个字段;老师类型也拥有姓名、课程、年龄三个字段。如此我们就可以通过该类型来存储学生和老师的信息了。
结构体实例化
当结构体实例化后,才会分配内存空间,才能使用结构体的字段。
结构体也是一种类型,我们可以像声明变量那样声明结构体类型。
go
var 结构体实例 结构体类型
type student struct {
name string
age int
}
func main() {
var s1 student
s1.name = "zhangsan"
s1.age = 18
fmt.Printf("%v\n", s1)
//{zhangsan 18}
fmt.Printf("%#v\n", s1)
//main.student{name:"zhangsan", age:18}
}
匿名结构体
匿名结构体常用于一些临时的数据结构上。
go
func main() {
var person struct{
name string;
age int
}
person.name = "张三"
person.age = 18
fmt.Println(person) //{张三 18}
}
指针类型结构体
我们可以通过new
来进行结构体的实例化,得到的是结构体的地址。
go
type student struct {
name string
age int
}
func main() {
var s1 = new(student)
fmt.Printf("%T\n", s1)
//*main.student
s1.name = "李四"
s1.age = 19
fmt.Printf("%#v\n", s1)
//&main.student{name:"李四", age:19}
}
当我们使用&
对结构体进行取地址后相当于对该结构体类型又进行了一次new
的实例化。
go
type student struct {
name string
age int
}
func main() {
s1 := &student{}
fmt.Printf("%T\n", s1)
//*main.student
fmt.Printf("%#v\n", s1)
//&main.student{name:"", age:0}
s1.name = "lisi"
s1.age = 19
fmt.Printf("%#v\n", s1)
//&main.student{name:"lisi", age:19}
}
结构体初始化
当我们声明一个结构体后,结构体的字段值都为各字段类型的默认零值,只有当我们进行初始化后才拥有各字段对应的值。
go
type student struct {
name string
age int
}
func main() {
s1 := student{}
fmt.Printf("%#v\n", s1)
//main.student{name:"", age:0}
//使用键值对结构体进行初始化
s2 := student{
name: "小明",
age: 20,
}
fmt.Printf("%#v\n", s2)
//main.student{name:"小明", age:20}
s3 := student{
name: "小美",
}
fmt.Printf("%#v\n", s3)
//main.student{name:"小美", age:0}
//使用值的列表进行初始化
//必须将所有结构体字段按照顺序依次进行初始化
s4 := &student{
"小红",
19,
}
fmt.Printf("%#v\n", s4)
//&main.student{name:"小红", age:19}
}
类型别名和自定义类型
类型别名
大家可以将类型别名理解为小名、外号。虽然名字不同,但是指向的都是同一个类型。
go
type byte = uint8
type rune = int32
自定义类型
在go语言中,我们可以使用type
来自定义类型,自定义类型其实就是定义了一个全新的类型。
大家可以理解为一对夫妇,生下了一个小男孩,然后为其取名为小明。
go
//将MyInt定义为int类型
type MyInt int
自定义类型和类型别名的区别
两者看似只有一个等号的差异。但是类型别名的类型依旧是原类型,而自定义类型的类型是新类型名。
go
//自定义类型
type newInt int
//类型别名
type myInt = int
func main() {
var a newInt
var b myInt
fmt.Printf("%T\n", a) //main.newInt
fmt.Printf("%T\n", b) //int
}
空结构体
当我们声明一个空结构体时,它本身是不占用任何内存空间的。
go
func main() {
var s struct{}
fmt.Println(unsafe.Sizeof(s)) //0
}
构造函数
学习过Java的小伙伴们一定都知道,当我们需要对一个对象进行初始化时,往往通过其的构造函数进行初始化。而go语言中并没有构造函数,但是我们可以通过函数来自己实现。由于struct
是值类型,值拷贝的性能消耗会比较大,所以我们通常使用结构体指针类型作为返回值。
go
type student struct {
name string
age int
}
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
func main() {
s1 := newStudent("zhangsan", 18)
fmt.Printf("%#v\n", s1)
//&main.student{name:"zhangsan", age:18}
}
方法和接收者
go语言中的方法
是一种作用于特点类型变量的函数。这种特定变量叫做接收者
。接收者分为指针类型接收者和值类型接收者两种。当需要修改接收者的属性时,一般使用指针类型接收者;当接收者占用内存过大时,一般也使用指针类型接收者。
go
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
type student struct {
name string
age int
}
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
func (s student) sleep() {
fmt.Printf("%v is sleeping in class\n", s.name)
}
func main() {
s1 := newStudent("zhangsan", 18)
s1.sleep() //zhangsan is sleeping in class
}
指针类型接收者
指针类型接收者常用于修改结构体中的字段时,例如需要给结构体字段设值的时候。修改后的值在整个方法内都有效,直到整个方法结束为止。
go
type student struct {
name string
age int
}
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
func (s *student) setAge(newAge int) {
s.age = newAge
}
func main() {
s1 := newStudent("zhangsan", 18)
fmt.Println(s1) //&{zhangsan 18}
s1.setAge(20)
fmt.Println(s1) //&{zhangsan 20}
}
值类型接收者
当方法作用于值类型接收者时,go语言会将接收者的值复制一份。在值类型接收者的方法中可以获取该接收者的成员值,但是只能在当前方法中使用。常用于结构体的行为方法。
go
type student struct {
name string
age int
}
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
func (s *student) setAge(newAge int) {
s.age = newAge
}
func (s student) sleep() {
fmt.Printf("%v is sleeping in class\n", s.name)
}
func main() {
s1 := newStudent("zhangsan", 18)
fmt.Println(s1) //&{zhangsan 18}
s1.setAge(20)
fmt.Println(s1) //&{zhangsan 18}
s1.sleep() //zhangsan is sleeping in class
}
任意类型添加方法
在go语言中,接收者的类型可以是任何类型,不仅是结构体,所有类型都可以添加方法。但是有个前提条件,该类型必须是自定义类型。
go
type myInt int
func (m myInt) say() {
fmt.Println("myInt 的自定义方法")
}
func main() {
var m1 myInt
m1.say()
//myInt 的自定义方法
}
结构体的匿名字段
在结构体声明时,字段名可以只声明类型,而没有字段名。
go
type student struct {
string
int
}
func main() {
s1 := student{
"zhangsan",
19,
}
fmt.Printf("%#v\n", s1)
//main.student{string:"zhangsan", int:19}
}
结构体嵌套
一个结构体中可以嵌套另外一个结构体。
go
type address struct {
province string
city string
}
type person struct {
name string
age int
address address
}
func main() {
p1 := person{
name: "张三",
age: 19,
address: address{
province: "江苏",
city: "苏州",
},
}
fmt.Printf("%#v\n", p1)
//main.person{name:"张三", age:19, address:main.address{province:"江苏", city:"苏州"}}
}
嵌套匿名函数
同样的,我们也可以在结构体中嵌套匿名字段。
go
type address struct {
province string
city string
}
type person struct {
name string
age int
address //匿名字段
}
func main() {
var p1 person
p1.name = "张三"
p1.age = 19
p1.address.province = "江苏" //默认使用类型名作为字段名
p1.city = "苏州" //匿名字段可以省略
fmt.Printf("%#v\n", p1)
//main.person{name:"张三", age:19, address:main.address{province:"江苏", city:"苏州"}}
}
嵌套结构体字段名冲突
当嵌套结构体的字段名和外部结构体的字段名冲突时,这个时候我们就需要指定具体的结构体字段名来指明字段。
go
type address struct {
time string
}
type email struct {
time string
}
type person struct {
time string
address //匿名字段
email //匿名字段
}
func main() {
var p1 person
p1.time = "2023"
fmt.Println(p1) //{2023 {} {}}
p1.address.time = "2022"
fmt.Println(p1) //{2023 {2022} {}}
p1.email.time = "2021"
fmt.Println(p1) //{2023 {2022} {2021}}
}
结构体继承
go语言可以通过嵌套匿名结构体来实现其他语言中的面向对象的继承。
go
type animal struct {
name string
}
func (a *animal) eat() {
fmt.Printf("%v正在吃东西\n", a.name)
}
type dog struct {
age int
*animal
}
func (d *dog) bark() {
fmt.Printf("%v岁的%v正在叫\n", d.age, d.name)
}
func main() {
d1 := &dog{
age: 3,
animal: &animal{ //嵌套结构体指针
name: "小白",
},
}
d1.eat() //小白正在吃东西
d1.bark() //3岁的小白正在叫
}
可见性
go语言中没有所谓的关键字来表示公有和私有,在go语言中,结构体字段首字母大写就表示可公开访问,小写表示私有。
小结
有了上篇的基础,我们又从本文中认识到go语言中的数组、切片、map集合、函数、指针类型以及结构体。希望小伙伴们能够有所收获。到目前为止,go语言的基础语法我们已经大致掌握,但是go语言的基础不止这些。敬请收看下篇《Go 语言入门指南:基础语法和常用特性解析 (下) 》。
码字不易,如果您看到了这里,听我说谢谢您。
如果您觉得本文还不错,还请留下您小小的赞。
如果您看了本文有所感受,还请留下您宝贵的评论。