Golang学习笔记三-函数与结构体

Golang 函数

Golang函数简介

函数的go语言中的一级公民 ,我们把所有的功能单元都定义在函数中,可以重复使用。函数包含函数的名称、参数列表和返回值类型,这些构成了函数的签名(signature)。

Go语言中函数特性

  1. go语言中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。receiver
  2. go语言中不允许函数重载(overload),也就是说不允许函数同名。
  3. go语言中的函数不能嵌套函数,但可以嵌套匿名函数。
  4. 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数。
  5. 函数可以作为参数传递给另一个函数。
  6. 函数的返回值可以是一个函数。
  7. 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
  8. 函数参数可以没有名称。

Go语言中函数的定义和调用

函数在使用之前必须先定义,可以调用函数来完成某个任务。函数可以重复调用,从而达到代码重用。

Go语言函数定义语法

1
2
3
4
func function_name( [parameter list] ) [return_types]
{
函数体
}

语法解析:

  • func:函数由func 开始声明
  • function_name:函数名称,函数名和参数列表一起构成了函数签名。
  • [parameter list]:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下return_types 不是必须的。
  • 函数体:函数定义的代码集合。

Go语言函数定义实例

定义一个求和函数

1
2
3
4
func sum(a int, b int) (ret int) {
ret = a + b
return ret
}

定义一个比较两个数大小的函数

1
2
3
4
5
6
7
8
func compare(a int, b int) (max int) {
if a > b {
max = a
} else {
max = b
}
return max
}

Go语言函数调用

当我们要完成某个任务时,可以调用函数来完成。调用函数要传递参数,如何有返回值可以获得返回值。

1
2
3
4
5
6
7
func main() {
s := sum(1, 2)
fmt.Printf("s: %v\n", s)

max := compare(1, 2)
fmt.Printf("max: %v\n", max)
}

运行结果

1
2
s: 3
max: 2

Golang函数的返回值

函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过 return关键字来指定。

return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。go中的函数可以有多个返回值。

  1. return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称
  2. 当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值
  3. 但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高
  4. 命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
  5. return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的。

Go语言函数返回值实例

没有返回值

1
2
3
func f1() {
fmt.Printf("我没有返回值,只是进行一些计算")
}

有一个返回值

1
2
3
4
func sum(a int, b int) (ret int) {
ret = a + b
return ret
}

多个返回值,且在return中指定返回的内容

1
2
3
4
5
func f2() (name string, age int) {
name = "老郭"
age = 30
return name, age
}

多个返回值,返回值名称没有被使用

1
2
3
4
5
func f3() (name string, age int) {
name = "老郭"
age = 30
return // 等价于return name, age
}

return覆盖命名返回值,返回值名称没有被使用

1
2
3
4
5
func f4() (name string, age int) {
n := "老郭"
a := 30
return n, a
}

Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如 return value,existsreturn value,okreturn value,err等。

当函数的返回值过多 时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。

但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线 _来丢弃这些返回值。例如下面的 f1函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量 a

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func f1() (int, int) {
return 1, 2
}
func main() {
_, x := f1()
fmt.Printf("x: %v\n", x)
}

运行结果

1
x: 2

Golang函数的参数

Go语言函数可以有0或多个参数,参数需要指定数据类型

声明函数时的参数列表叫做形参,调用时传递的参数叫做实参。

Go语言是通过传值的方式传参 的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。

Go语言可以使用变长参数 ,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用 ARGS...TYPE的方式。这时会将 ...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。

Golang函数的参数实例

Go语言传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 形参列表
func f1(a int, b int) int {
if a > b {
return a
} else {
return b
}
}

func main() {
// 实参列表
r := f1(1, 2)
fmt.Printf("r: %v\n", r)
}

演示参数传递,按值传递

1
2
3
4
5
6
7
8
9
10
func f1(a int) {
a = 200
fmt.Printf("a1: %v\n", a)
}

func main() {
a := 100
f1(a)
fmt.Printf("a: %v\n", a)
}

运行结果

1
2
a1: 200
a: 100

从运行结果可以看到,调用函数f1后,a的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。

mapsliceinterfacechannel这些数据类型本身就是指针 类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能 会影响外部数据结构的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func f1(a []int) {
a[0] = 100
}

func main() {
a := []int{1, 2}
f1(a)
fmt.Printf("a: %v\n", a)
}

运行结果

1
2
a: [1 2]
a: [100 2]

从运行结果发现,调用函数后,slice内容被改变了。

变长参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func f1(args ...int) {
for _, v := range args {
fmt.Printf("v: %v\n", v)
}
}
func f2(name string, age int, args ...int) {
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
for _, v := range args {
fmt.Printf("v: %v\n", v)
}
}
func main() {
f1(1, 2, 3)
fmt.Println("------------")
f1(1, 2, 3, 4, 5, 6)
fmt.Println("------------")
f2("tom", 20, 1, 2, 3)
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v: 1
v: 2
v: 3
------------
v: 1
v: 2
v: 3
v: 4
v: 5
v: 6
------------
name: tom
age: 20
v: 1
v: 2
v: 3

Golang函数类型与函数变量

可以使用 type关键字来定义一个函数类型,语法格式如下:

1
type fun func(int, int) int

上面语句定义了一个 fun函数类型,它是一种函数类型,这种函数接收两个 int类型的参数,并且返回一个 int类型的返回值。

下面我们定义两个这样结构的两个函数,一个求和,一个比较大小:

1
2
3
4
5
6
7
8
9
10
11
func sum(a int, b int) int {
return a + b
}

func max(a int, b int) int {
if a > b {
return a
} else {
return b
}
}

下面定义一个 fun函数类型,把 summax赋值给它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type fun func(int, int) int

func sum(a int, b int) int {
return a + b
}

func max(a int, b int) int {
if a > b {
return a
} else {
return b
}
}

func main() {
var f fun
f = sum
s := f(1, 2)
fmt.Printf("s: %v\n", s)
f = max
m := f(3, 4)
fmt.Printf("m: %v\n", m)
}

运行结果

1
2
s: 3
m: 4

Golang高阶函数

go语言的函数,可以作为函数的参数,传递给另外一个函数,可以可以作为,另外一个函数的返回值返回。

Go语言函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func sayHello(name string) {
fmt.Printf("Hello,%s", name)
}

func f1(name string, f func(string)) {
f(name)
}

func main() {
f1("tom", sayHello)
}

运行结果

1
Hello,tom

Go语言函数作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

func add(x, y int) int {
return x + y
}

func sub(x, y int) int {
return x - y
}

func cal(s string) func(int, int) int {
switch s {
case "+":
return add
case "-":
return sub
default:
return nil
}
}

func main() {
add := cal("+")
r := add(1, 2)
fmt.Printf("r: %v\n", r)

fmt.Println("-----------")

sub := cal("-")
r = sub(100, 50)
fmt.Printf("r: %v\n", r)
}

运行结果

1
2
3
r: 3
-----------
r: 50

Golang匿名函数

Go语言函数不能嵌套,但是在函数内部可以定义匿名函数,实现一下简单功能调用。

所谓匿名函数就是,没有名称的函数。

语法格式如下:

1
func (参数列表)(返回值)

当然可以既没有参数,可以没有返回值

匿名函数实例

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
max := func (a int, b int) int {
if a > b {
return a
} else {
return b
}
}

i := max(1, 2)
fmt.Printf("i: %v\n", i)
}

运行结果

1
i: 2

自己执行

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// 自己执行
func(a int, b int) {
max := 0
if a > b {
max = a
} else {
max = b
}
fmt.Printf("max: %v\n", max)
}(1, 2)
}

运行结果

1
max: 2

Golang闭包

闭包可以理解成定义在一个函数内部的函数 。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。 首先我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

// 返回一个函数
func add() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}

func main() {
var f = add()
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30))
fmt.Println("-----------")
f1 := add()
fmt.Println(f1(40))
fmt.Println(f1(50))
}

运行结果

1
2
3
4
5
6
10
30
60
-----------
40
90

变量 f是一个函数并且它引用了其外部作用域中的 x变量,此时 f就是一个闭包。 在 f的生命周期内,变量 x也一直有效。 闭包进阶示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func add(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = add(10)
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30))

fmt.Println("----------")

f1 := add(20)
fmt.Println(f1(40))
fmt.Println(f1(50))
}

运行结果

1
2
3
4
5
6
20
40
70
----------
60
110

闭包进阶示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"))
fmt.Println(txtFunc("test"))
}

运行结果

1
2
test.jpg
test.txt

闭包进阶示例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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))
fmt.Println(f1(3), f2(4))
fmt.Println(f1(5), f2(6))
}

运行结果

1
2
3
11 9
12 8
13 7

闭包其实并不复杂,只要牢记 闭包=函数+引用环境

Golang递归

函数内部调用函数自身的函数称为递归函数。

使用递归函数最重要的三点:

  1. 递归就是自己调用自己。
  2. 必须先定义函数的退出条件,没有退出条件,递归将成为死循环。
  3. go语言递归函数很可能会产生一大堆的goroutine,也很可能会出现栈空间内存溢出问题。

Go语言递归实例

阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func a(n int) int {
// 返回条件
if n == 1 {
return 1
} else {
// 自己调用自己
return n * a(n-1)
}
}

func main() {
n := 5
r := a(n)
fmt.Printf("r: %v\n", r)
}

运行结果

1
r: 120

斐波那契数列

它的计算公式为 f(n)=f(n-1)+f(n-2)f(2)=f(1)=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

func f(n int) int {
// 退出点判断
if n == 1 || n == 2 {
return 1
}
// 递归表达式
return f(n-1) + f(n-2)
}

func main() {
r := f(5)
fmt.Printf("r: %v\n", r)
}

运行结果

1
r: 5

Golang defer语句

go语言中的 defer语句会将其后面跟随的语句进行延迟 处理。在 defer归属的函数即将返回时,将延迟处理的语句按 defer定义的逆序 进行执行,也就是说,先被 defer的语句最后被执行,最后被 defer的语句,最先被执行。stack

defer特性

  1. 关键字defer 用于注册延迟调用。
  2. 这些调用直到return 前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放

Go语言defer语句实例

查看执行顺序

1
2
3
4
5
6
7
func main() {
fmt.Println("start")
defer fmt.Println("step1")
defer fmt.Println("step2")
defer fmt.Println("step3")
fmt.Println("end")
}

运行结果

1
2
3
4
5
start
end
step3
step2
step1

Golang init函数

Golang有一个特殊的函数 init函数,先于 main函数执行,实现包级别的一些初始化 操作。

init函数的主要特点

  • init函数先于main函数自动执行 ,不能被其他函数调用;
  • init函数没有输入参数、返回值;
  • 每个包可以有多个init函数;
  • 包的每个源文件也可以有多个init函数 ,这点比较特殊;
  • 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
  • 不同包的init函数按照包导入的依赖关系决定执行顺序。

Golang 初始化顺序

初始化顺序:变量初始化->init()->main()

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

var a int = initVar()

func init() {
fmt.Println("init2")
}

func init() {
fmt.Println("init")
}

func initVar() int {
fmt.Println("init var...")
return 100
}

func main() {
fmt.Println("main...")
}

运行结果

1
2
3
4
init var...
init2
init
main...

Golang指针

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。

类型指针不能进行偏移和运算。

Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和 *(根据地址取值)。

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用 &字符放在变量前面对变量进行取地址 操作。 Go语言中的值类型 (int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

指针语法

一个指针变量指向了一个值的内存地址。(也就是我们声明了一个指针之后,可以像变量赋值一样,把一个值的内存地址放入到指针当中。)

类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

1
var var_name *var-type

var-type :为指针类型

var_name :为指针变量名

* :用于指定变量是作为一个指针。

指针声明实例

1
2
var ip *int        /* 指向整型*/
var fp *float32 /* 指向浮点型 */

指针使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}

运行结果

1
2
3
a 变量的地址是: c00000a0a8
ip 变量储存的指针地址: c00000a0a8
*ip 变量的值: 20

Golang指向数组的指针

定义语法

1
var ptr [MAX]*int; 表示数组里面的元素的类型是指针类型

实例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

const MAX int = 3

func main() {
a := []int{ 1, 3, 5}
var i int
var ptr [MAX]*int;
fmt.Println(ptr) //这个打印出来是[<nil> <nil> <nil>]
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] ) //*ptr[i]就是打印出相关指针的值了。
}
}

运行结果

1
2
3
4
[<nil> <nil> <nil>]
a[0] = 1
a[1] = 3
a[2] = 5

Golang类型定义和类型别名

在介绍结构体 之前,我们先来看看什么是类型定义和类型别名。

Go语言类型定义

类型定义的语法

1
type NewType Type

实例

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
// 类型定义
type MyInt int
// i 为MyInt类型
var i MyInt
i = 100
fmt.Printf("i: %v i: %T\n", i, i)
}

运行结果

1
i: 100 i: main.MyInt

Go语言类型别名

类型别名的语法

1
type NewType = Type

实例

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
// 类型别名定义
type MyInt2 = int
// i 其实还是int类型
var i MyInt2
i = 100
fmt.Printf("i: %v i: %T\n", i, i)
}

Go语言类型定义和类型别名的区别

  1. 类型定义相当于定义了一个全新的类型 ,与之前的类型不同;但是类型别名并没有定义一个新的类型,而是使用一个别名来替换之前的类型
  2. 类型别名只会在代码 中存在,在编译完成 之后并不会存在该别名
  3. 因为类型别名和原来的类型是一致的,所以原来类型所拥有的方法 ,类型别名中也可以 调用,但是如果是重新定义的一个类型,那么不可以 调用之前的任何方法。

Golang 结构体

go语言没有面向对象的概念了,但是可以使用结构体来实现,面向对象编程的一些特性,例如:继承、组合等特性。

Go语言结构体的定义

上一节我们介绍了类型定义,结构体的定义和类型定义类似,只不过多了一个 struct关键字,语法结构如下:

1
2
3
4
5
6
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}

type:结构体定义关键字

struct_variable_type:结构体类型名称

struct:结构体定义关键字

member definition;:成员定义

实例

下面我们定义一个人的结构体Person

1
2
3
4
5
6
type Person struct {
id int
name string
age int
email string
}

以上我们定义一个Person结构体,有四个成员,来描述一个Person的信息。

形同类型的可以合并到一行 ,例如:

1
2
3
4
type Person struct {
id, age int
name, email string
}

声明一个结构体变量

声明一个结构体变量和声明一个普通变量相同,例如:

1
2
3
4
var tom Person
fmt.Printf("tom: %v\n", tom)
kite := Person{}
fmt.Printf("kite: %v\n", kite)

也行结果

1
2
tom: {0 0  }
kite: {0 0 }

结构体成员,在没有赋值之前都是零值。

访问结构体成员

可以使用点运算符(.),来访问结构体成员,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
type Person struct {
id, age int
name, email string
}

var tom Person
tom.id = 1
tom.name = "tom"
tom.age = 20
tom.email = "[email protected]"
fmt.Printf("tom: %v\n", tom)
}

运行结果如下

1
tom: {1 20 tom [email protected]}

匿名结构体

如果结构体是临时使用,可以不用起名字,直接使用,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
var dog struct {
id int
name string
}
dog.id = 1
dog.name = "花花"
fmt.Printf("dog: %v\n", dog)
}

Golang结构体的初始化

未初始化的结构体,成员都是零值 int 0 float 0.0 bool false string nil nil

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
type Person struct {
id, age int
name, email string
}

var tom Person
fmt.Printf("tom: %v\n", tom)
}

运行结果

1
tom: {0 0 "" ""}

使用键值对对结构体进行初始化

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
type Person struct {
id, age int
name, email string
}

kite := Person{
id: 1,
name: "kite",
age: 20,
email: "[email protected]",
}
fmt.Printf("kite: %v\n", kite)
}

运行结果

1
kite: {1 20 kite [email protected]}

使用值的列表初始化

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
type Person struct {
id, age int
name, email string
}

kite := Person{
1,
20,
"kite",
"[email protected]",
}
fmt.Printf("kite: %v\n", kite)
}

运行结果

1
kite: {1 20 kite [email protected]}

注意:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。

部分成员初始化

用不到的成员,可以不进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
type Person struct {
id, age int
name, email string
}

kite := Person{
id: 1,
name: "kite",
}
fmt.Printf("kite: %v\n", kite)
}

运行结果

1
kite: {1 0 kite "" }

Golang结构体指针

结构体指针和普通的变量指针相同,我先来回顾一下普通变量的指针,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
var name string
name = "tom"
// p_name 指针类型
var p_name *string
// &name 取name地址
p_name = &name
fmt.Printf("name: %v\n", name)
// 输出指针地址
fmt.Printf("p_name: %v\n", p_name)
// 输出指针指向的内容值
fmt.Printf("*p_name: %v\n", *p_name)
}

运行结果

1
2
3
name: tom
p_name: 0xc00010e120
*p_name: tom

Go结构体指针

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
type Person struct {
id int
name string
}

var tom = Person{1, "tom"}

var p_person *Person
p_person = &tom
fmt.Printf("tom: %v\n", tom)
fmt.Printf("p_person: %p\n", p_person)
fmt.Printf("*p_person: %v\n", *p_person)
}

运行结果

1
2
3
tom: {1 tom}
p_person: 0xc000004078
*p_person: {1 tom}

使用 new关键字创建结构体指针

我们还可以通过使用 new关键字对结构体进行实例化,得到的是结构体的地址,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
type Person struct {
id int
name string
}

var p_person = new(Person)
fmt.Printf("p_person: %T\n", p_person)
}

运行结果

1
p_person: *main.Person

从运行结果,我们发现p_person为指针类型

访问结构体指针成员

访问结构体指针成员,也使用点运算符(.),例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
type Person struct {
id int
name string
}

var p_person = new(Person)
fmt.Printf("p_person: %T\n", p_person)

p_person.id = 1
p_person.name = "tom"
fmt.Printf("*p_person: %v\n", *p_person)
}

运行结果

1
2
p_person: *main.Person
*p_person: {1 tom}

Golang结构体作为函数参数

go结构体可以像普通变量一样,作为函数的参数,传递给函数,这里分为两种情况:

  1. 直接传递结构体,这是是一个副本(拷贝),在函数内部不会改变外面结构体内容。
  2. 传递结构体指针,这时在函数内部,能够改变外部结构体内容。

直接传递结构体

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Person struct {
id int
name string
}

func showPerson(person Person) {
person.id = 1
person.name = "kite"
fmt.Printf("person: %v\n", person)
}

func main() {
person := Person{1, "tom"}
fmt.Printf("person: %v\n", person)
fmt.Println("----------------")
showPerson(person)
fmt.Println("----------------")
fmt.Printf("person: %v\n", person)
}

运行结果

1
2
3
4
5
person: {1 tom}
----------------
person: {1 kite}
----------------
person: {1 tom}

从运行结果可以看出,函数内部改变了结构体内容,函数外面并没有被改变。

传递结构体指针

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Person struct {
id int
name string
}

func showPerson(person *Person) {
person.id = 1
person.name = "kite"
fmt.Printf("person: %v\n", person)
}

func main() {
person := Person{1, "tom"}
fmt.Printf("person: %v\n", person)
fmt.Println("----------------")
showPerson(&person)
fmt.Println("----------------")
fmt.Printf("person: %v\n", person)
}

运行结果

1
2
3
4
5
person: {1 tom}
----------------
person: &{1 kite}
----------------
person: {1 kite}

从运行结果,我们可以看到,调用函数后,参数被改变了。

Golang嵌套结构体

go语言没有面向对象编程思想,也没有继承关系,但是可以通过结构体嵌套来实现这种效果。

下面通过实例演示如何实现结构体嵌套,加入有一个人 Person结构体,这个人还养了一个宠物 Dog结构体。

下面我们来看一下:

Dog结构体

1
2
3
4
5
type Dog struct {
name string
color string
age int
}

Person结构体

1
2
3
4
5
type person struct {
dog Dog
name string
age int
}

访问它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

type Dog struct {
name string
color string
age int
}

type person struct {
dog Dog
name string
age int
}

func main() {
var tom person
tom.dog.name = "花花"
tom.dog.color = "黑白花"
tom.dog.age = 2

tom.name = "tom"
tom.age = 20

fmt.Printf("tom: %v\n", tom)
}

运行结果

1
tom: {{花花 黑白花 2} tom 20}

Golang方法

go语言没有面向对象的特性,也没有类对象的概念。但是,可以使用结构体来模拟这些特性,我们都知道面向对象里面有类方法等概念。我们也可以声明一些方法,属于某个结构体。

Go语言方法的语法

Go中的方法,是一种特殊的函数 ,定义于struct之上(与struct关联、绑定),被称为struct的接受者(receiver)。

通俗的讲,方法就是有接收者的函数。

语法格式如下:

1
2
3
4
type mytype struct{}

func (recv mytype) my_method(para) return_type {}
func (recv *mytype) my_method(para) return_type {}

mytype:定义一个结构体

recv:接受该方法的结构体(receiver)

my_method:方法名称

para:参数列表

return_type:返回值类型

从语法格式可以看出,一个方法和一个函数非常相似,多了一个接受类型。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type Person struct {
name string
}

func (per Person) eat() {
fmt.Println(per.name + " eating....")
}

func (per Person) sleep() {
fmt.Println(per.name + " sleep....")
}

func main() {
var per Person
per.name = "tom"
per.eat()
per.sleep()
}

运行结果

1
2
tom eating....
tom sleep....

Go语言方法的注意事项

  1. 方法的receiver type并非一定要是struct类型,type定义的类型别名、slice、map、channel、func类型等都可以。
  2. struct结合它的方法就等价于面向对象中的类。只不过struct可以和它的方法分开,并非一定要属于同一个文件,但必须属于同一个包。
  3. 方法有两种接收类型:(T Type)(T *Type),它们之间有区别。
  4. 方法就是函数,所以Go中没有方法重载(overload)的说法,也就是说同一个类型中的所有方法名必须都唯一。
  5. 如果receiver是一个指针类型,则会自动解除引用。
  6. 方法和type是分开的,意味着实例的行为(behavior)和数据存储(field)是分开的,但是它们通过receiver建立起关联关系。

Golang方法接收者类型

结构体实例,有值类型和指针类型,那么方法的接收者是结构体,那么也有值类型和指针类型。区别就是接收者是否复制结构体副本。值类型复制,指针类型不复制。

值类型结构体和指针类型结构体

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

type Person struct {
name string
}

func main() {
p1 := Person{name: "tom"}
fmt.Printf("p1: %T\n", p1)
p2 := &Person{name: "tom"}
fmt.Printf("p2: %T\n", p2)
}

运行结果

1
2
p1: main.Person
p2: *main.Person

从运行结果,我们可以看出p1是值类型,p2是指针类型。

下面看一个传参结构体的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type Person struct {
name string
}

func showPerson(per Person) {
fmt.Printf("per: %p\n", &per)
per.name = "kite"
fmt.Printf("per: %v\n", per)
}

func showPerson2(per *Person) {
fmt.Printf("per: %p\n", per)
per.name = "kite"
fmt.Printf("per: %v\n", per)
}

func main() {
p1 := Person{name: "tom"}
fmt.Printf("p1: %p\n", &p1)
showPerson(p1)
fmt.Printf("p1: %v\n", p1)
fmt.Println("---------------")
p2 := &Person{name: "tom"}
fmt.Printf("p2: %p\n", p2)
showPerson2(p2)
fmt.Printf("p2: %v\n", p2)
}

运行结果

1
2
3
4
5
6
7
8
9
p1: 0xc000046240
per: 0xc000046250
per: {kite}
p1: {tom}
---------------
p2: 0xc000046280
per: 0xc000046280
per: &{kite}
p2: &{kite}

从运行结果,我们看到p1是值传递,拷贝了副本,地址发生了改变,而p2是指针类型,地址没有改变。

方法的值类型和指针类型接收者

值类型和指针类型接收者,本质上和函数传参道理相同。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type Person struct {
name string
}

func (per Person) showPerson() {
fmt.Printf("per: %p\n", &per)
per.name = "kite"
fmt.Printf("per: %v\n", per)
}

func (per *Person) showPerson2() {
fmt.Printf("per: %p\n", per)
per.name = "kite"
fmt.Printf("per: %v\n", per)
}

func main() {
p1 := Person{name: "tom"}
fmt.Printf("p1: %p\n", &p1)
p1.showPerson()
fmt.Printf("p1: %v\n", p1)
fmt.Println("---------------")
p2 := &Person{name: "tom"}
fmt.Printf("p2: %p\n", p2)
p2.showPerson2()
fmt.Printf("p2: %v\n", p2)
}

运行结果

1
2
3
4
5
6
7
8
9
p1: 0xc000046240
per: 0xc000046250
per: {kite}
p1: {tom}
---------------
p2: 0xc000046280
per: 0xc000046280
per: &{kite}
p2: &{kite}

从运行结果,我们看到p1是值传递,拷贝了副本,地址发生了改变,而p2是指针类型,地址没有改变。