Golang: Golang 代码走廊

学习 Golang 的时候写了 N 个demo,这里是对它们的温习和讲解。(可作为新手的练习指导)

这里其实是单独把 Go1.9 语言学习中写过的例子,全部搬到这里,当然是按逻辑整理的。

参考: Go 圣经, go-by-examples 等等。 后续有一篇代码荟萃摘取一些常见场景的代码精粹。

已经完全分类完毕, 几乎涵盖 Go 的方方面面。(注意1.9版本有些内容和之前版本的Go是不同的)

关于练习:

这里要求只有一个,把别人的代码看1-2遍,然后自己写,在不回头看。

一旦中途回头看了,刚刚写过的全部删除,重头开始。(毕竟都是小例子)

本文既是练习,也是知识点讲解,主要围绕下面的关键字:

内置关键字

建议: 步步为营,一个知识点没有非常熟悉的时候,不要进到下一个知识点。(特别是 struct, method, interface这类有牵扯的内容)

Main函数

  • main函数既没有参数,也没有返回值 (命令行参数,另外有方法)
  • package名为 main 的才可以包含 main函数
  • 一个可执行程序,必须要有一个 main 包
  • 一般使用驼峰命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

//常量定义(常量注释格式不对)
const PI = 3.14
//类型声明
type type_name int
//结构声明
type struct_name struct{
//定义成员
name string
}
//接口声明
type interface_name interface{
//ur code here
}

func main() {
fmt.Println("你好世界, hello 中国")
}

import别名

  • import 的时候,可以使用别名 import alias_name real_name, 或者使用.代替(不建议, 容易混淆)
1
2
3
4
5
6
7
package main

import std "fmt"

func main() {
std.Println("hello");
}

可见性规则

外部是否可见,是否能够调用,采用大小写约定

  • 函数首字母小写即不可以被外部调用,大写意味着可以被外部调用(exported).
  • 变量,常量,类型,接口,结构规则类似,首字母小写的话,外部不可见.
  • 可见性是相对于包而言的,同一个包内,无隐私.

所以看到,fmt的好多方法一定是首字母大写的

组定义

多个常量,变量,类型等,可以放在一个组里面,一次性定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//常量
const (
PI = 3.14
const1 = "1"
const2 = 2
const3 = 3
)

//全局变量
var (
name = "bob"
name1 = "xp"
name2 = "xpbob"
)

上面这些貌似,可以根据赋值自动推到出类型。

1
2
var num int  // var num int = 0
var num = 10 // var num int = 10
  • var组 只能用于声明全局变量(函数体内则不能用这种方法)

语法上就是: (记着 var 开头就对了)

一般类型也可以按组来定义:

1
2
3
4
5
6
type (
newType int
type1 float32
type2 string
type3 byte
)

之后就可以拿这些类型去定义变量了,例如var aa newType,一般用于单位转换。

定义变量,var 关键字也可以省略:

1
2
3
var num int =10
//等价于
num := 10

第二种形式,省略掉了 var 关键字,并且类型也要由系统自动推断。

  • 一般而言,如果声明和使用都是一致的,那么就可以交给系统去推断
    • 并且,系统鼓励你省略类型
  • 声明时就赋值,那么直接采用最简写法即可
  • 全部变量不能省略 var 关键字!

局部变量虽然不能用 var(), 但是可以并排写:

1
2
3
4
5
6
7
8
9
var a, b, c, d int
a, b, c, d = 1, 2, 3, 4

//或者声明同时赋值
var a, b, c, d int = 1, 2, 3, 4
//等价于(省略类型的情况)
var a, b, c, d = 1, 2, 3, 4
//等价于(省略 var )
a, b, c, d := 1, 2, 3, 4

对局部变量来说 : 是用来代替 var 的,但是全局变量不能有此特权

:= 一般用于函数有多个返回值的时候,直接定义的同时接收数据

基本类型

简单介绍整型:

别名 byte, rune更多的在是在强调其作用情景。

  • 当你用 byte 的时候,可能是用来处理字节相关的内容
  • 当你用 rune 的时候,可能是用来处理 unicode 相关的字符

浮点型:

  • float32/float64, 分别是4字节和8字节,精确到小数点后7/15位
  • double? 不存在的

复数类型:

  • complex64/complex128, 分别占8/16字节

math包可以检查数据的取值范围, 例如 math.MinInt8 打印出来 -128。

其他类型

  • 引用类型 : slice(可以认为切片是数组的封装), map, chan(协程通道)
  • 接口类型 : interface
  • 函数类型 : func (因为,函数可以赋值给变量)
  • 其他 : array(可变数组类型), struct(结构类型), string(字符串类型)

map 可以简单的认为是一个 Hash 表, 但是其实现与其他语言有所不同;使用起来也有所不同。

零值

零值并不等于空值,而是当变量被声明为某种类型后的默认值,通常情况下值类型的默认值为0,bool为false,string为空字符串.

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

import "fmt"

func main() {
//验证一下各种类型的零值
var cnt int
fmt.Println(cnt) //0

var bb []byte
fmt.Println(bb) //[]


var intArr [3]int //不指定大小就是 slice
fmt.Println(intArr) //[0,0,0]

var flag bool
fmt.Println(flag) //false
}

隐式转换

Go语言没有隐式转换,只有强制类型转换。

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

import "fmt"


func main() {
var b int
b = 1 //ok
b = 1.1 //编译报错: constant 1.1 truncated to integer
fmt.Println(b)
}

类型转换

所有类型间的转换,必须是显示的转换,并且发生在兼容的类型之间。

<value A> [:]= <type B>(<value B>)

举个例子: (int 和 rune转换)

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

import "fmt"

func main() {

var a rune = 10
var b int

//b = a //编译报错 : cannot use a (type rune) as type int in assignment
b = int(a) //强制类型转换

fmt.Println(b)
}

非bool 和 bool 之间不兼容!(数字和逻辑之间不兼容)

string 和 其他类型呢?

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

import "fmt"

func main() {
a := 65
str := string(a)

fmt.Println(str)//打印字符A, 而不是65
}

可能它默认你需要的是这个数字表示的字符,其他类型和 string 转换借助 strconv 包。

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

import (
"strconv"
"fmt"
)

func main() {
var a = 65
str := strconv.Itoa(a)

//a = strconv.Atoi(b)
fmt.Println(str) //65
}

空白符号

一个下划线 _ 用来赋值忽略

例子:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
a, _, c := 1, 2, 3
fmt.Println(a) //1
fmt.Println(c) //3
}

这么直白的看上去,似乎有点儿傻;但是一般用于函数多返回值的情景。

常量

首先说一下,下面要讲解的常量&枚举,应该使用大写

这里使用小写主要为了演示用。(同时,使用小写字母开头或者下划线开头,禁止其他包访问)

常量的值,在编译时就已经确定了。

  • 定义格式和变量类似,除了 const 关键字,能省略的都可以省略
  • 可以使用 const() 组语法定义一组常量
    • 如果不指定的话,则输出默认赋值和上一行一样 (看下面的例子),而不是零值
    • 组内第一个常量的定义,表达式不能省略,必须初始化
    • 给定了类型就要给出表达式(expression)不能用零值
  • 可以并排定义常量, 即仅用一个 const ,然后按顺序初始化
  • 右侧必须是内置函数(编译时就能确定值)

demo 如下:

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
35
36
package main

import "fmt"

//常量
const defalt_salary int = 10000

//默认常量
const default_age = 20

//常量组
const (
name = "bob"
age = 22
salary = defalt_salary
married //如果不指定,则是上一行的表达式
)

//多赋值
const a, b = 1, 1.1


//套用上一行 + 多赋值
const (
c, d = 2, "2,2"
e, f
)

func main() {
fmt.Println(married) //输出 10000
fmt.Println(a) // 1
fmt.Println(b) // 1.1

fmt.Println(e) //2
fmt.Println(f) //2.2
}

常量一定要加 const 修饰

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

import "fmt"

var str = "xxxx" //把常量赋值给变量str

func main() {

//length := len(str) //ok
const length = len(str) //编译报错 : const initializer len(str) is not a constant
fmt.Println(length)
}

因为 len(str) 在编译时确定不了值。(实际上是因为 str 是变量,运行时才能确定内容)

枚举

枚举借助 const(), 但是要借助iota常量计数器。

上面说了,const()中如果不赋值,那么就和上一行保持一样的赋值行为;
但是当你赋值为 iota 时,下面开始自动赋值, 核心规则如下:

  • iota 从零开始计数 (记录组里的常量, 从组里第一个从常量开始记录)
  • 同一组中每增加一个常量 iota 计数器加1 (反正它就在组里默默增加,而不是你每使用一次iota才增加计数)
  • 遇到 const 关键字,计数器回复为0 (即开始定义下一个组的常量了)

代码如下:

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

import "fmt"

const (
a = "A"
b
c = iota //c值为2,即取得计数器
d //d值为3
)

func main() {

fmt.Println(a) //A
fmt.Println(b) //A
fmt.Println(c) //2
fmt.Println(d) //3

num := iota //编译报错: undefined: iota
}

单独使用 iota, 编译报错;一定要用在 const() 组里

运算符

运算符

^ 比较特殊,要看一下运算符左右有几个变量。

演示:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import "fmt"

func main() {
//取反
fmt.Println(!true) //false
//fmt.Println(!1) //编译报错: ! untyped number

//^当一元运算符
fmt.Print("^一元运算: ")
fmt.Println(^1) //-2 结果比较奇怪, 用的比较少

/* 位运算符 */
//与运算符
fmt.Println(1&0) //0
//或运算
fmt.Println(1|0) //1

//亦或
fmt.Println(1^0) //1
fmt.Println(0^1) //1
fmt.Println(1^1) //0
fmt.Println(0^0) //0
//fmt.Println(false^false); //编译报错:^ not defined on bool

fmt.Println("-----special-----")
//&^
//主要看第二个操作数:
//如果第二个操作数是0,那么结果和第一个操作数的位相同;
//第二个操作数的位是1,那么结果一定是0
fmt.Println(1&^0) //1
fmt.Println(0&^1) //0
fmt.Println(0&^0) //0
fmt.Println(0&^1) //0
fmt.Println(1&^1) //0

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

//乘除,取余-略

// << 和 >>
fmt.Println(1<<10) // 1024
fmt.Println(8>>3) //1


//逻辑运算符 (带短路)
fmt.Println(false && true) //false
fmt.Println(true && false) //false
fmt.Println(false || true) //true

}

关于位移运算,这里有一个经典的应用:

1
2
3
4
5
6
7
8
9
10
11
const (
_ = iota
KB = 1 << (10 * iota) //1024
MB
GB
TB
PB
EB
ZB
YB
)

或者下面的定义方式:

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

import "fmt"


const (
B = 1 << (iota * 10)
KB
MB
GB
TB
PB
)

func main() {
fmt.Println(B)
fmt.Println(KB)
fmt.Println(MB)
}

递增递减

++ 和 – 作为语句而不是作为表达式

  • 不能放在等号左边儿并且需要单独的一行;而表达式则可以放在等号右边
  • 只能放在变量的右边,而不能在左边 (作为一个语句)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
a := 1
//b := a++ //编译报错: syntax error: unexpected ++ at end of statement
//b := ++a //编译报错: syntax error:unexpected ++, expecting expression

//++a //报错: unexpected ++, expecting }, 不能放在变量的左边
a++ //只能放在变量的右边
b := a

fmt.Println(b)
}

指针

这里指针运算比较滑稽,不支持 -> 运算,而是统一采用 . 运算来操作。

基本用法还是类似:

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

import "fmt"

func main() {
//var a int = 1
a := 1
//var b *int = &a
b := &a

fmt.Println(b); //打印地址
fmt.Println(*b); //打印值

//默认值是 nil, 而不是 NULL
var p *int
fmt.Println(p) //<nil>
}

有没有指针运算

貌似也没有。

if条件控制语句

最大的区别:

  • 连括号都懒得写了; if 后面写一个空格再写 condition
  • condition 之前,支持一个初始化表达式(用分号阻隔)

下面写一个例子:

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

import "fmt"

func main() {
a := 1

//不带括号
if a > 0 {
fmt.Println("a 可以继续参加运算.");
}

//支持一个初始化表达式, 注意是分号
if b:=1; a == b {
fmt.Println("a 和 b 相等.");
}

//b是局部变量,出了上述作用于范围就嗝屁了
//fmt.Println(b); //编译报错: undefined: b
}

注意,局部变量还是会隐藏范围更大的变量。

switch条件控制语句

switch 语句被增强了: (带有条件判断)

  • switch 支持一个初始化表达式(可以是并行方式,即同时按顺序初始化多个,最右侧要写分号)
  • switch 后面可以不写值
  • case 可以是单一值,多值或者 condition条件
  • 不用写 break 语句,执行完一个 case 会自动跳出去(跳出整个 switch)
  • 如果想接着执行,那么要写 fallgroup

大致就这三种形式:

switch

相关 demo 看下来

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"

func main() {

i := 5
switch {
case 0<=i && i<=5:
fmt.Println("0-5")
case 6<=i && i<=10:
fmt.Println("6-10")
}

switch i{
case 0,1,2,3,4,5:
fmt.Println("0-5")
case 6,7,8,9,10:
fmt.Println("6-10")
}

var j int

switch j, i=1, 1; {
case i==j :
fmt.Println("j==b");
fallthrough
default:
fmt.Println("fallthrough 继续执行");
}
}

switch 中初始化的变量,同样是局部变量

上面没有写经典的 switch。

循环语句

Go语言中只保留了一种循环, for 循环,但是却有3种形式, 非常强大。

其实就是把 while 循环合并到 for 循环里面了

  • 完全不带有条件的 (无限循环,在for内部满足条件break出去)
  • 带有条件的 for (单个条件的就相当于 while 循环)
  • 普通的for (3部分齐全)

for循环

1.无限循环,在for内部满足条件break出去:

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
package main

import "fmt"

func main() {

b := 2

for {
if b == 2 {
fmt.Println("b==2, 第一次循环");
b--
} else if b ==1 {
fmt.Println("b==1, 直接 continue 吧");
b--
continue
} else {
fmt.Println("b==0, 顺序执行下去算了")
}

if b==0 {
fmt.Println("已经执行到最后流程了,出去吧")
break
}
}//for
}

实际上,这种 for 就是: for true {}

长得像while的for循环:

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

import "fmt"

func main() {
//带有单个条件的 for 循环

a := 1
for a > 0 {
fmt.Println("循环中...");
a--
}
fmt.Println("循环结束了...")
}

完整的for循环:

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

import "fmt"

func main() {
var sum int
for i :=1 ; i <= 100; i++ {
sum += i
}
fmt.Println(sum) //5050
}

总结下来,非常灵活;需不需要条件,完全自己看着办。

还有一个 for each, 即 range for, 用于遍历 slice、map、数组、字符串等。

例如: 遍历数组

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

import "fmt"

func main() {
//简单演示一下 range for

arr := [5]int{1,2,3,4,5}

//使用 range 关键字,后面跟着容器的名字
//可以把 range 当做一个多返回值的函数

/*
for key, value := range oldMap {
newMap[key] = value
} */
for index,value := range arr {
fmt.Printf("index=%d, value=%d\n", index, value);
}
}

跳转语句

老三样: goto, break, continue

  • 都可以配合标签 (跳出内层循环,可以连跳基层;但一定配合循环)
  • 标签名区分带小写(严格匹配)
  • goto是调整执行位置,不一定非要用在循环中;但也要配置标签。

但是 break, continue 还是原来的意思。

例子: 外层是死循环的,只跳出一层循环肯定不行.

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

import "fmt"

func main() {
LABEL1:
for { //外层是个死循环
for i:=0; i < 10; i++ {
if i==1 {
break LABEL1;
}
}
}
fmt.Println("跳出了与 LABEL 同级的循环")
}

contine 的例子也是调到外层 label 同级的循环,但是会进行下一次的外层循环。
contine外部循环应该是个有限循环.

Label 标签一定要紧接 for 语句

goto 语句比较难用,要慎用。(如果一定要用,那么请把goto的label放到goto后面,避免造成死循环)

个人建议,用最简单的break和continue吧.

数组

其实用的更多的应该是 slice .

格式: var <varName> [n]<type>,其中 n > 0。

数组必须在定义的时候指定长度,否则不称之为数组:

  • 数组长度也是类型的一部分,因此具有不同长度的数组为不同类型
    • 不同长度的数组其实不同类型,不能直接赋值(还是要遍历赋值)
  • 数组在Go中为值类型 (也就是说,基本都是值拷贝,不能改变原来的内容)
  • 数组之间可以使用==或!=进行比较(系统已经实现这种运算),但不可以使用<或>
  • 可以使用new来创建数组,此方法返回一个指向数组的指针
  • Go支持多维数组
  • 长度不知道,可以用 `…``代替,但是还是要指定(不指定长度就是 slice)

简单的使用:

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
package main

import "fmt"

func main() {
var a [2]int
//var b [1]int //长度不同,不能相互赋值
var b [2]int //长度相同则可以

a = b //长度不同,编译报错: cannot use b (type [1]int) as type [2]int in assignment
fmt.Println(a);

//如果不默认赋值,那么系统自动补零值
//var aa int[3]
aa := [3]int{1}
fmt.Println(aa)

//也可以按照索引指定(索引后面跟着冒号)
bb := [5]int{3:1} //第4个元素为1,其他为0
fmt.Println(bb)

//逐个指定
cc := [3]int{1:2, 2:3}
fmt.Println(cc)


//...指定长度 (根据后面初始化的元素,确定长度)
// 如果指定索引,那么也会根据索引来推断长度
words := [...]string{"hello", "world"}
for _, word := range words {
fmt.Println(word)
}
}

指针数组和数组指针问题。

数组指针:

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

import "fmt"

func main() {

/* 数组指针 */
a := [10]int{2:1, 3:1}
//声明一个数组指针(写完数组类型,前面补一个 *号即可)
var p *[10]int
//p = a //编译报错:cannot use a (type [10]int) as type *[10]int in assignment
p = &a //数组是值类型, 应该用取地址符号

fmt.Println(p)
fmt.Println(*p)
}

指针数组:

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

import "fmt"

func main() {
/* 指针数组:数组的元素都是地址 */

x, y := 1, 2
//先写好数组,然后把数组的元素类型改为 *int
array := [2]*int {&x, &y}
fmt.Println(array); //这样遍历只能拿到地址

for _,value := range array {
fmt.Println(*value)
}
}

数组是值类型,而不是引用类型,所以传递参数的时候,其实是进行拷贝的(而不是地址)

如果要降低拷贝代价,请使用 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 main() {
a := [2]int{1,2}
b := [2]int{1,2}
c := [2]int{2,3}
d := [1]int{0}

fmt.Println(a==b) //true
fmt.Println(a==c) //false
//fmt.Println(a==d) //不同类型的比较,编译报错
//invalid operation: a == d (mismatched types [2]int and [1]int)

aa := [...]string{"hello"}
bb := [...]string{"hello"}
cc := [...]string{"hello", "world"}

fmt.Println(aa==bb) //true
//fmt.Println(aa==cc) //不同类型的比较,编译报错
//invalid operation: aa == cc (mismatched types [1]string and [2]string)
}

通过下标操作数组:

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

import "fmt"

func main() {
arr1 := [3]int{1,2,3}
arr1[2] = 4

fmt.Println(arr1)
}

多维数组:

多维数组一样可以使用下标运算, 索引指定等。
只有第一维可以用 … 指定长度(自动计算),最好自己指定。

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

import "fmt"

func main() {
arr := [...][3]int {
{1,2,3},
{1}}
//}//编译报错
fmt.Println(arr)
}

注意最后一个} 必须在最后一个元素的同一行,否则编译报错

其他关于数组的练习,可以写一个简单点写一个冒泡排序:

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 main() {
arr := [5]int{2,8,4,9,5}
fmt.Println(arr) //排序之前

//降序排序吧
leng := len(arr)
for i := 0; i < leng; i++ {
for j := i+1; j < leng; j++ {
if arr[i] < arr[j] {
//交换 保证 当前的 arr[j]是最小
tmp := arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
} //inner for
} //outer for
fmt.Println(arr)
}

new关键字

可以通过 new 关键字拿到相关类型的地址。

比如:

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

import "fmt"

func main() {
p := new([3]int)
fmt.Println(*p)

p[2] = 4
fmt.Println(*p)
}

slice切片

这是一个相对而言比较新的内容,其实也还好。

  • 变长数组的替代方案,可以指向数组的局部或者全部
  • 可以用于指向一个已经存在的数组或者直接声明一个slice
  • 形式上和数组不指定长度一样

不太正式的使用方法,一般使用方法:

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

import "fmt"

func main() {
var sla []int //不指定长度即是 slice
fmt.Println(sla)

//已经存在一个数组了, 用slice截取后面的元素
arr := [10]int{1,2,3,4,5,6,7,8,9,10}
fmt.Println(arr)
//sli := arr[1] //取一个元素

//sli := arr[5:10] //含头不含尾 index 5-9, index 10 不包含
//等价于//sli := arr[5:len(arr)]
sli := arr[5:] //也是取 index 5-9, 直接取到末尾
fmt.Println(sli)
}

取前5个元素的话 arr[:5]

正式使用方法:

make([]T, len, cap)

一般不使用new, 其中 len() 可以获取 len, cap()可以获取cap容量。
这里容量都是直接翻倍的,仅当不够用的时候.

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

import "fmt"

func main() {
//使用 make 方法
slice := make([]int, 10, 15) //指定长度为10,容量为15
fmt.Println(slice) //打印整个 slice
fmt.Println(len(slice), cap(slice)) //10, 15
}
  • 可以不指定容量(系统自动重分配)

Rslice: 简单说,就是截取现有的 slice,从一个 slice 中获得一个新的 slice。

  • 每一次取 slice 索引就又重新规划一次 (从0开始)。
  • 新的 slice 可能长度有限,但是容量是截取的开始到原来 slice 的末尾(底层还是那个数组)
  • 新的 slice 的最大索引不能超过原 slice 的 cap (如果按索引取值越界,会引发错误;但不会导致底层数组重新分配)
  • 由于都共享底层数组,那么一个slice的改变可能会引起另外一个slice值改变

slice2array

这里 cap 总归是 11, 但是 slice_a 从第三个开始取,所以它的最大容量就是 9。
并且,如果slice_b要从slice_a中开始取,那么索引应该从0重新排号。

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

import "fmt"

func main() {
oldSlice := []byte{'a','b','c','d','e'} //5个元素
sliceA := oldSlice[0:3] //取a, b ,c
fmt.Println(string(sliceA))
sliceB := oldSlice[3:5] //取 d,e
fmt.Println(string(sliceB))

//sliceC 是在 sliceB 的基础上取
sliceC := sliceB[1:] //取e
fmt.Println(string(sliceC))

//看看 len 和 cap
fmt.Println(len(oldSlice), cap(oldSlice)) //5 5
fmt.Println(len(sliceA), cap(sliceA)) //3, 5
fmt.Println(len(sliceB), cap(sliceB)) //2, 2
fmt.Println(len(sliceC), cap(sliceC)) //1, 1
}

SliceAppend:

  • 使用 append 函数
  • 可以在尾部追加元素
  • 可以在尾部追加slice

(追加完成了,如果没有超过cap,那么返回原来slice,否则返回重新分配的slice,同时拷贝元素)
(重新分配,一般是原来的 cap 翻倍)

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

import "fmt"

func main() {
//用标准写法
s1 := make([]int, 3, 4)
fmt.Printf("%v, %p\n", s1, s1) //查看地址

//开始追加
s1 = append(s1, 1, 2) //现在长度为5已经超过了cap4,所以翻倍为8
fmt.Println(cap(s1), len(s1))
fmt.Printf("%v, %p\n", s1, s1) //查看地址(发现已经改变)

}
  • 如果重新分配过了,那么一个slice的修改不会影响原来的slice。(因为底层数组不再是同一个了)
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
package main

import "fmt"

func main() {
arr := []int{1,2,3}
s1 := arr[0:2]
s2 := arr[1:3]

fmt.Println(s1) //1, 2
fmt.Println(s2) //2, 3

//s1和s2共享一个 2 元素,修改试试看(同时改变)
s2[0] = 4
fmt.Println(s1) //1, 4
fmt.Println(s2) //4, 5

//看看s2的cap
fmt.Println(cap(s2))

//但是在 s2 背后添加呢?
s2 = append(s2, 2, 3, 4) //重新分配然后拷贝过去, 底层数组已经不一样了
fmt.Println(s2)

//在修改s2已经影响不了s1了 (底层数据结构不一样了)
s2[0] = 9
fmt.Println(s1) //1, 4
fmt.Println(s2) //9, 3, 2, 3,4
}

copy()函数:

如果不想在一个 slice 尾部追加,那么可以用copy进行覆盖。

但是拷贝时注意类型一定要一致,并且长度不同,效果不同。

下面有个案例:

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

import "fmt"

func main() {
s1 := []int{1,2,3,4}
s2 := []int{5,6,7}

//copy(s1,s2) //s2拷贝到s1
//fmt.Println(s1) //s1 变成 5,6,7,4

copy(s2, s1) //s1拷贝到s2 (原来的 slice 长度保持不变, 所以只拷贝3个元素)
fmt.Println(s2) //1,2,3
//选择性拷贝
copy(s2, s1[1:]) //2,3,4 拷贝到 s2 覆盖
fmt.Println(s2)

//完全拷贝?请直接赋值
s3 := s1
fmt.Println(s3)
}

也就是说,长度不够的时候,以短的为准。(多余元素抛弃)source 和 target 都可以用索引截取.

如果要完全拷贝,可以用索引截取,但也可以直接 reslice,即赋值的方式。

map

这个 map, 类似其他语言的哈希表或者字典,就是 key-value。

  • key 必须支持 ==!= 运算 (不能是函数,map, slice啥的)
  • value 没有类型限制(包括函数类型都行), value可以是空值
  • map和slice一样使用 make 创建(cap 可以省略), 支持 := 简写方式

make([keyType]valueType, cap) 其中 cap 表示容量,可以省略(超出容量自己扩容)

  • len() 获取 map 的元素个数, 即 key 的个数。
  • delete() 删除某键值对
  • range for 进行迭代
  • 存储的话,通过key,和数组赋值类似
  • 多返回值, 第一个返回值就是 value, 第二返回值表明该key是否存在
    • 如果是空值,有可能该key本身不存在;或者存在,但存的是空值(所以有必要检查一下)

简单演示:

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"

func main() {
//全写
var m1 map[int]string //定义
m1 = map[int]string{} //初始化
fmt.Println(m1) //map[]

//或者使用 make
var m2 map[int]string = make(map[int]string, 3) //cap 可以省略
fmt.Println(m2)

//make也可以简写 (系统也建议你简写)
m3 := make(map[int]string)
fmt.Println(m3)

/*添加元素*/
m1[1] = "Ok"
fmt.Println(m1[1])

/*覆盖, 重复添加的话,就直接覆盖了*/
m1[1]="Cancel"
fmt.Println(m1[1])

/*删除, by key*/
delete(m1, 1)
fmt.Println(m1[1]) //打印为空

}

复杂的例子: (主要复杂在 value 类型上,这里是 map类型)

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

import "fmt"

func main() {
complex_map := make(map[int]map[int]string) //value的类型是 map[int]string

//先取一下值,看看存不存在
str, ok := complex_map[1][1]
if !ok {
//说明第一个key还没有插入,即没有值; 那就创建一下
complex_map[1] = make(map[int]string)
//现在可以插入了
complex_map[1][1] = "Ok"
}

//取出来看看
str, ok = complex_map[1][1]
fmt.Println(str, ok)
}

遍历试试看: (对key, value副本修改不会影响原来的map)

3个元素的slice, 每个元素都是map,遍历试试看。

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
package main

import "fmt"

func main() {
//操作副本不会影响原来的对象
sli := make([]map[int]string, 3, 4) //长度为3, cap为4的slice
//遍历slice进行初始化
for _,value := range sli {
value = make(map[int]string, 1)
value[1]="OK"
fmt.Println(value)
}
fmt.Println(sli) //这里是空的,表明是对副本操作了。

//直接操作原始 sli
//遍历slice进行初始化
for i := range sli {
sli[i] = make(map[int]string, 1)
sli[i][1]="OK"
fmt.Println(sli[i])
}
fmt.Println(sli)

}

map的key排序:

map 遍历时拿出来 key 的顺序是不固定的(除非你以map[key]指定拿哪个元素)
借助 sort slice 可以对拿出来的 key 进行排序,从而间接对 map 排序了

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

import "fmt"
import "sort"

func main() {
m1 := map[int]string{1:"a", 2:"b", 3:"c", 4:"d", 5:"e"}
//初始化的时候貌似有序,但是遍历时发现是无序的
for k,v := range m1 {
fmt.Println(k,v) //多运行几次,发现是无序的
}

//此时可以把 key 放入容器中,对 key 排序,从而拿到对 value 排序的目的
//意思是,如果slice有序,那么map一定有序
i := 0
sli := make([]int, 5, 5)
for k := range m1 {
sli[i] = k
i++
}
fmt.Println(sli)
sort.Ints(sli) //排序 int 类型的容器
fmt.Println(sli)
}
  • sort.Ints(slice) 表示排序 int 型的 slice 。

函数

  • 函数不支持重载,默认参数; 当然也不能在函数里定义函数(不支持嵌套)
  • 使用之前,不一定要声明
  • 多返回值 (没有返回值的话,在返回值的地方可以不写; 单个返回值不需要小括号)
  • 支持匿名函数/闭包
  • 相同类型的参数, 返回值可以简写 (a, b, c int), 可以用不定长变参
  • 命名返回值参数(相同类型简写的情况, return写明返回类的数据;否则直接写return)
    • 最好加上返回值的名称
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 main() {
a, b := f(1)
fmt.Println(a, b)

c, d := f1()
fmt.Println(c,d)
}

func f(param1 int)(result1, result2 int) {
result1, result2 = param1, 0
return //因为已经给返回值命名了,系统知道要返回的是result1, reuslt2
}

func f1()(int, int) {
//这里返回值没有命名, 所以下面要指定
result1, result2 := 1, 0
return result1, result2
}

为了代码可读,最好 return 后面加上返回值的名称

不定长变参:

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
package main

import "fmt"

func main() {
f(1,2,3,4)

//变参实际上是值拷贝
a, b, c := 1, 2, 3
f1(a, b, c)
fmt.Println("外部: ", a, b, c)
}

//不定长变参函数 (必须作为最后一个参数, 编译器的要求)
func f(a ...int) { //没有返回值,可以直接不写
fmt.Println(a)
}

func f1(a ...int) {
//如果传递的是值,那么内部修改并不会影响外部的值
a[0] = 3
a[1] = 2
a[2] = 1
fmt.Println("内部: ", a)
}

值拷贝 & 可变参数在参数列表最后

参数传递问题: (引用传递,值拷贝)

  • 这里把拷贝地址称为引用传递
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
35
36
package main

import "fmt"

func main() {

a := 1
b := &a
c := []int{1,1}

f1(a)
fmt.Println("外部a:", a)


f2(b)
fmt.Println("外部b:", *b) //被内部修改

f3(c)
fmt.Println("外部c:", c) //被内部修改
}

func f1(a int) {
a = 0
fmt.Println("内部a", a)
}

func f2(b *int) {
*b = 0
fmt.Println("内部b", *b)
}

func f3(c []int) { //传递slice
c[0] = 0
c[1] = 0
fmt.Println("内部c", c)
}

函数作为类型: (一切皆类型)

把函数作为类型赋值给变量

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

import "fmt"

func main() {

// A 函数调用了 AA 函数
A()
}

func A() {
a := AA
a() //相当于函数调用 AA()
}

func AA() {
fmt.Println("调用AA")
}

匿名函数: (就是定义的时候不给名字,其他一切如常)

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

import "fmt"

func main() {
//匿名函数的调用
a := func () { //这里相当于给匿名函数命名, a是函数类型
fmt.Println("我是匿名函数")
}
a()
}

闭包:

C语言返回给比人一个函数指针,这里闭包可以返回一个函数体&代码块

闭包可以捕获外部变量,跟 lambda 一样

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

import "fmt"

func main() {
//闭包返回一段代码块
codeBlock := closure(1) //返回类型 codeBlock 的类型是 func(int) int
result1 := codeBlock(0)
result2 := codeBlock(2)
fmt.Println("result1 = ", result1)
fmt.Println("result2 = ", result2)
}

func closure(x int) func(int) int {
//closre(x)闭包返回 func(int) int 代码块
return func(y int) int {
fmt.Println("x = ", x)
fmt.Println("y = ", y)
return x+y
}
}
  • 看闭包的返回类型即可知道封装的 code block 类型
  • 闭包相对于匿名函数的好处是,拿到闭包的时候,它可以捕获外部的变量;
  • 实际调用闭包封装的 code block 是才真正传入形参

仔细看,实际上经过闭包;原来的函数 closure 变成了另外一个函数调用

给函数套个封装

闭包捕获的是不是原始变量本身? 不是。默认值拷贝

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

import "fmt"

func main() {
x := 1
fmt.Printf("main函数: x地址 = %p\n", &x) //0xc420012090
codeBlock := closure(x)
codeBlock(3.14)
}

//探究一下 闭包捕获的变量是不是原始变量? 看来默认值拷贝。

func closure(x int) func(float32)float32 {
fmt.Printf("闭包内部: x地址 = %p\n", &x) //0xc420012098
return func(y float32) float32 {
fmt.Printf("Code Block内部: x地址 = %p\n", &x) //0xc420012098
return y
}
}

我试试按照指针来一波: (这回捕获的不再是值拷贝了)

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

import "fmt"

func main() {
x := 1
fmt.Printf("main函数: x地址 = %p\n", &x) //0xc420012090
codeBlock := closure(&x)
codeBlock(3.14)
}

//探究一下 闭包捕获的变量是不是原始变量? 看来默认值拷贝。

func closure(x *int) func(float32)float32 {
fmt.Printf("闭包内部: x地址 = %p\n", x) //0xc420012090
return func(y float32) float32 {
fmt.Printf("Code Block内部: x地址 = %p\n", x) //0xc420012090
return y
}
}

defer关键字

有点儿 cpp raii按照block析构变量的意思。用的很少,但用起来也简单.

在函数执行结束后,按照调用顺序的相反顺序逐个执行 defer 指定的内容.

  • 先用 defer 定义的先入栈,后定义的后入栈;出栈则相反
  • 即使函数发生严重错误也会执行;有点儿 finally 的意思
  • 支持匿名函数调用 (也就是说可以给defer指定匿名函数)
  • defer本意是调用某个函数,所以指定匿名函数的之后,最后要加上()

主要应用场景: (由于涉及到异常和错误,所以用好defer还是不错的)

  • 资源清理
  • 文件关闭
  • 解锁
  • 记录事件

简单的演示:

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

import "fmt"

func main() {
defer fmt.Println("执行 1")
defer fmt.Println("执行 2")
fmt.Println("main函数执行")
A()
}

func A() {
fmt.Println("执行 3")
fmt.Println("子函数执行")
}

可以看到,确实是函数执行完毕之后,倒序执行 defer 定义的语句。

defer 内容一定是主调函数完毕才去执行的

特别是碰到匿名函数中引用了外部变量值时(默认还是值拷贝), 下面有三个案例,按顺序看:

匿名函数:

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
package main

import "fmt"

func main() {
a := 1
a = 2

b := 5
b = 6

defer func(b int) {
fmt.Printf("defer b: %d, %p\n", b, &b)
fmt.Printf("defer a: %d, %p\n", a, &a)
}(b)

a = 3
fmt.Printf("outer a: %d, %p\n", a, &a)
b = 7
fmt.Printf("outer b: %d, %p\n", b, &b)
}

/** a是直接拿,b是通过值拷贝参数传递
outer a: 3, 0xc420012090 //a 地址相同
outer b: 7, 0xc420012098 //b 地址不同
defer b: 6, 0xc4200120c0
defer a: 3, 0xc420012090
**/
  • 先看b, 匿名函数参数,默认还是值拷贝;所以地址不同,匿名函数内部拿到的也只是defer指定时的拷贝
  • 在看a, 匿名函数直接取,地址相同,值是 main 执行完毕时那个状态(用在for range时只能取到容器最后一个元素)

有名函数,子函数的调用呢?

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

import "fmt"

func main() {
b := 1
b = 2
defer subFunc(b) //0xc4200120b0
b = 3
fmt.Printf("outer b: %v, %p\n", b, &b) //0xc420012090
}


func subFunc(b int) {
fmt.Printf("defer b: %v, %p\n", b, &b)
}

/*运行结果: (通过参数传递的,所以地址不同;defer时拿到的是一份拷贝)
outer b: 3, 0xc420012090
defer b: 2, 0xc4200120b0
*/
  • 参数值传递, 拿到的是 defer 制定时的副本

但是内置函数有点奇葩:

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

import "fmt"

func main() {
a := 1
a = 2
defer fmt.Printf("defer a: %v,%p\n", a, &a) //defer a: 2,0xc420012090
a = 3
fmt.Printf("outer a: %v,%p\n", a, &a) //outer a: 3,0xc420012090
}

原则上:

  • 是同一个地址的话,应该取函数运行结束时的值(比如:匿名函数直接拿到&使用的就是同一个地址,所以值是末尾执行完毕的值)
  • 副本地址的话,就是defer指定时的副本(比如匿名函数的参数,子函数的参数;拿到的都是副本,所以地址不同且值也是当时的副本)
  • 判断直接使用,简单的方法是语句块&闭包里面找不到它的定义(此时初步可以认定就是从外部直接拿到的,是同一个地址)

但是Print系列函数虽然是子函数调用(含参数传递),地址按原来的取,值则是拷贝

defer虽然看起来就像一个注册功能,但是真正运行的时候,根据不同代码有不同的表现。

这里有一个笔试题。

笔试题

panic/recover模式

Go语言没有异常机制,但是通过 panic/recover 这种模式可以错误异常、错误。

panic 可以在任何地方引发,但 recover只有在defer指定时有效

下面引发一个 panic, recover试试。

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"

func main() {
//碰到 panic 后面基本就终止了
defer fmt.Println("先声明")
A()
B()
C()
defer fmt.Println("后声明") //后声明的没有执行

}

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

func B() {
fmt.Println("func B")
//下面引发一个panic
panic("发生严重错误了")
}

func C() {
fmt.Println("func C")
}
  • panic 之后的代码一律不能执行,包括后面指定的 defer 代码
  • panic 之前注册的 defer 才能在引发异常之后执行

recover的注册代码要写在 panic 引发之前,最好在同一个调用栈里, 即哪个函数可能会引发,自己提前做好处理

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
35
package main

import "fmt"

func main() {
//碰到 panic 后面基本就终止了
defer fmt.Println("我只处理main函数内的异常")
A()
B()
C()

}

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

func B() {
fmt.Println("func B")

//引发问题前做好异常处理
defer func() {
if flag := recover(); flag != nil {
//表明确实引发了异常
fmt.Println("函数B能引发了异常,我来恢复")
}
}()

//下面引发一个panic
panic("发生严重错误了") //制定了recover就不会panic了
}

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

通过 拿到 recover() 的返回值来判断是否引发了异常,然后进行处理

有些错误就是很严重,没有 recover 的可能(不能修复,只能终止),这个时候就需要panic.

struct结构

type <struct_name> sturct{}

花括号里面定义结构的字段、属性,它可以是一般类型,引用类型或者别的struct类型。

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 empty struct {}

type person struct {
name string
age int
}

func main() {
ee := empty{}
fmt.Println(ee)

pp := person {}
fmt.Println(pp)
pp.name = "bob"
pp.age = 20
fmt.Println(pp)

pp1 := person{"bob",22, //直接字面值指定
} //字面值直接初始化(括号必须换行)
fmt.Println(pp1)

pp2 := person{ //指定索引
name:"mike",
age:23, //直接字面量初始化
}
fmt.Println(pp2)
}
  • 直接字面量初始化
  • 指定索引初始化

但是上面有几个问题没有涉及到:

  • 嵌套 struct 类型怎么办?
    • 组合模型下要拿到父类的字段怎么办?
    • 嵌套的结构有相同字段怎么办?
  • 指针传递问题?
  • 匿名 struct ?
    • 嵌套匿名定义 (结构定义时匿名的,但字段是有名的,只能之后按字段赋值)
    • 字段匿名
  • struct只有属性么?方法呢?—这个单独再说(看下一节)

先说简单的指针问题 :

struct的指针,直接拿,直接用

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
35
36
37
38
package main

import "fmt"

type person struct {
name string
age int
}

func main() {
//默认的传参数,都是值传递;不说

//way1, 先构建一个 person 对象,然后取地址
p1 := person {
name: "bob",
age: 20, //不要忘记逗号
}
setPerson(&p1)
fmt.Println("outer: ", p1)


//way2 -- 直接拿到struct的指针
p2 := &person {
name: "mike",
age:30,
}
setPerson(p2)
fmt.Println("outer: ", *p2)//这里p2是指针

//指针也用 . 操作
p2.age = 32
fmt.Println("outer: ", *p2)
}

func setPerson(p *person){
p.age += 1
fmt.Println("inner: ", *p)
}
  • 指针传递,裸指针,注意安全(智能指针也要注意)
  • 指针传递一般比值传递性能要高,拷贝代价低

推荐直接取结构地址,用指针操作

匿名结构:

结构的字段没有定义的情况下,就想直接使用; 并且只用一次(只在本次拿到实例就完事儿了)

  • 定义结构的同时进行初始化
  • 匿名就不需要 type 来给 struct{} 起别名了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
//定义的同时初始化(临时定义)
pp := &struct {
name string
age int
}{
name: "bob",
age: 12,
}
fmt.Println(*pp)
}

嵌套问题:

匿名结构也可以被嵌套:

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

import "fmt"

type person struct {
name string
age int
contact struct { //这里contac是person的字段,该结构的实例,而该结构是没有名字的
city, dic string //同类型可以一起定义
/* city string
disc string */
}
}

func main() {
pp := & person{}
fmt.Println(*pp) //{ 0 { }}
}
  • 定义完struct,之后按照index或者name进行初始化
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
35
package main

import "fmt"

type person struct {
name string
age int
contact struct {
city, dic string
}
}

func main() {
//way1: 定义之后再指定初始化
pp1 := & person{name:"joe", age:19}
fmt.Println(*pp1) //{joe 19 { }}
pp1.contact.city = "上海"
pp1.contact.dic = "黄浦区"
fmt.Println(*pp1)

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

//way2: 定义的时候就指定好---失败了
/* pp2 := &person {
name: "mike",
age: 20,
contact : {
city: "上海", dic: "黄浦区",
},
} */

//way2: 定义的时候就制定好,全部直接字面量,不用字段指定---失败了
/* pp2 := &person {"mike", 20, {"上海", "黄浦区"}} // missing type in composite literal
fmt.Println(*pp2) */
}

也就是说,如果在结构里定义了匿名结构,初始化只能在外部进行。(编译器能力有限, 识别不了你的花样)

如果是嵌入的有名结构,则可以定义实例的时候就指明:

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
35
package main

import "fmt"

type human struct {
name string
age int
}

type student struct {
human
stuNo int
}

type teacher struct {
human
teaNo int
}

func main() {

//定义就初始化--直接字面量(这也解释了为啥内部匿名嵌套定义不能这么初始化)
ss := student{human{"bob",10}, 1} //ok
fmt.Println(ss)

//定制就初始化--按照字段指定
ss2 := student {
human: human{
name: "bob",
age: 10,
},
stuNo: 1,
}
fmt.Println(ss2)
}
  • 按照字段指定的时候,内部嵌套struct类型被当做了字段用于声明
  • 嵌入结构的字段,可以直接拿下来使用 (如果没有二义性问题,即从两个嵌入结构拿到同样的字段的情况)
  • 本层字段会隐藏嵌入结构的同名字段,如果要用要明确指定嵌入结构(名称冲突)
1
2
3
4
5
6
7
//操作嵌入式结构的字段
ss2.human.age = 11
fmt.Println(ss2)

//相当于把嵌入结构字段----不推荐这样,容易引起歧义
ss2.age = 12
fmt.Println(ss2)

嵌套的结构有相同的字段:

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
35
36
package main

import "fmt"

type A struct {
name string
}

type B struct {
name string
}

type C1 struct {
A
B
}

type C2 struct {
A
B
name string
}

func main() {
cc1 := C1{} //ok
fmt.Println(cc1)

//编译报错
//fmt.Println(cc1.name) //ambiguous selector cc1.name
fmt.Println(cc1.A.name) //ok

cc2 := C2{} //ok
fmt.Println(cc2)
fmt.Println(cc2.name) //ok

}
  • 最高级别如果不存在某个字段,那么找内部嵌入的strut的字段

匿名字段:

  • 定义的时候按顺序初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type person struct {
string
int
}

func main() {
//way1 按顺序初始化
pp := &person {"mike", 20}
//pp := &person {20, "mike"}//编译报错
fmt.Println(*pp)

//way2 ???
}

按规矩做事情,还是不要搞匿名字段比较好。

struct实例的比较运算:

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
package main

import "fmt"

type person struct {
name string
age int
}

type person1 struct {
name string
age int
}

func main() {
pp0 := person {
name: "bob",
age:20,
}
pp1 := person {
name: "bob",
age:20,
}
fmt.Println("pp0 == pp1, ", pp0 == pp1) //true

pp1.age += 1
fmt.Println("pp0 == pp1, ", pp0 == pp1) //false

//bb0 := person1 {"bob", 20}
//fmt.Println("bb0 == pp0, ", bb0 == pp0)
//编译报错:mismatched types person1 and person
}
  • 相同类型结构才可以比较

类型方法

给 struct 或者其他类型添加方法,其实就是给方法指定默认的 receiver。

指定该方法的所属对象?并不是,看下面的结论。

个人猜想,方法指定 receiver 应该是调用的时候默认传入 this 指针。(猜想错了)

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
35
36
37
package main

import "fmt"

type person struct {
name string
age int
}

func main() {
pp := person{"bob", 10}
fmt.Println("old: pp", pp)

pp.printPerson1()
fmt.Println("outer print1: pp", pp) //内部改变,但外部没有变

pp.printPerons2()
fmt.Println("outer print2: pp", pp)
}
//指定 receiver
func (p person) printPerson1() { //默认按值拷贝
p.age ++
fmt.Println("inner print1", p.name, p.age)
}

func (p *person) printPerons2() { //指定传递 person 对象的指针
p.age ++
fmt.Println("inner print2", p.name, p.age)
}

/* 输出结果
old: pp {bob 10}
inner print1 bob 11
outer print1: pp {bob 10}
inner print2 bob 11
outer print2: pp {bob 11}
*/
  • 如果指定 receiver 是值方式,那么就是对象属性的拷贝
  • 如果指定 receiver 是指针方式,那么就是引用原来的对象

实际上指定的 receiver 是函数的第一个参数(所以可以通过函数的方式调用)

Go方法,没有重载,没有泛型,所以针对不同的类型,要针对不同receiver分别声明.

(如果你想根据参数不同来弄个重载,肯定报错,例子略)

  • 有些类型不能直接指定方法,但是通过给类型别名指定方法,则可以间接为原来的类型指定方法
  • 内置类型不能直接指定,因为这里的int类型不是本包定义的;这里的指定放大只适用于本包定义的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

//类型别名,然后给别名指定方法
type XX int

func main() {
var x XX
x.printInt()
}

//给 XX 指定方法

func (p *XX)printInt() {
*p = *p + 1 //因为 X 就是 int
fmt.Println(*p)
}

一般基本类型或者不能直接指定方法的类型,都要借助类型别名。(相当于绕过语法限制)

  • 类型别名指定的方法只能通过别名的实例调用,而不能被原来的类型实例直接调用
  • 类型别名和原来的类型混合计算的时候,要把原来的类型强转成类型别名的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

//类型别名,然后给别名指定方法
type XX int

func main() {
var x XX
x.setInt(3)
}

//给 XX 指定方法
func (p *XX)setInt(num int) {
//*p += num //编译报错 // mismatched types XX and int
/* tmp := XX(num)
*p += tmp */
*p += XX(num)
fmt.Println(*p)
}

实际上指定的 receiver 是函数的第一个参数,所以可以通过函数的方式调用:

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

import "fmt"


type XX int

func main() {
var x XX
//x.printInt()

//通过 method express调用
(*XX).printInt(&x) //传入实例的指针
}

//给 XX 指定方法

func (p *XX)printInt() {
*p = *p + 1 //因为 X 就是 int
fmt.Println(*p)
}

通过函数调用 Vs 通过函数的方式调用,建议还是用实例调用吧。

接口

无侵入式设计的接口

早有耳闻无侵入式的接口,也就是说,不需要改变原来类、结构、或者任何类型的任何部分,就可以说它具有某些接口的feature。

在定义结构的时候根本不需要声明什么关键字说它实现了某个接口,只需要在给它指定方法的时候,指定某个接口的方法即可。(必须为某个类型指定某个接口内的全部方法后,放能说这个类型实现了这个接口)

简单说: 为类型指定接口里的方法即可。(structural typing)

  • 接口只有方法声明,没有实现
  • 接口没有数据字段

(下面代码有一个注意事项: 如果USB接口里有一个方法 name(),它会和 PCWire 的字段重名,导致编译报错)

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import "fmt"

type USB interface {
getName() string //定义一个方法,参数为空,返回string类型
connect()
disconnect()
}

//定义一个结构,PC数据线,实现了USB接口
type PCWire struct {
name string
}

func main() {
var usb USB
usb = PCWire{"PC 数据线"} //usb的运行时类型是 struct PCWire
//usb.name undefined (type USB has no field or method name)
//usb.name = "PC 数据线"//应该在上一步赋值的时候指定
str := usb.getName()
fmt.Println(str) //PC 数据线

//一般的用法,不用定义接口实例,直接指定实现接口的类型实例
usb2 := PCWire{"劣质 PC 数据线"}
usb2.connect()
usb2.disconnect()

usb3 := PCWire{"高级 PC 数据线"}
(*PCWire).connect(&usb3)
(*PCWire).disconnect(&usb3)

}

//实现了usb意味着三个方法都要实现

//receiver 可以指定对象或者指针
func (wire PCWire) getName() string {
return wire.name
}

func (wire PCWire) connect() {
fmt.Println(wire.name + "已经连接 USB")
}

func (wire PCWire) disconnect() {
fmt.Println(wire.name + "已经断开USB")
}

可以仔细看 usb2 := PCWire{"劣质 PC 数据线"}, 根本没有定义接口,直接用的struct实例(如果它实现了某接口,那么直接用相关的方法即可,不用废话)。接口不用声明,直接使用。其实这里是包容性原则:

抽象层级高的可以包容抽象层级低的事物

当然也可以有这种操作: (把接口当做接收实例的形参)

1
2
3
4
5
6
7
//来一个形参方法,接收 USB 实例 (面向接口编程)
func ensureInterface(usb USB) {
fmt.Println("正常调用该方法,说明传入的实参是 usb 的实例")
fmt.Println(usb.getName()) //直接 usb.name 不行,usb没有这个字段
//fmt.Println(PCWire(usb).name) //强制类型转换?
//编译报错:cannot convert usb (type USB) to type PCWire: need type assertion
}
  • 子类或者具体类型不能向抽象类型转换,不兼容。(抽象包容具体是ok的)

不能强制类型转换,但是我就是想从实例那儿拿到字段怎么办?判断一下运行时类型 type assertion.

1
2
3
4
5
6
7
8
9
10
//来一个形参方法,接收 USB 实例 (面向接口编程)
func ensureInterface(usb USB) {

//fmt.Println(PCWire(usb).name) //强制类型转换,报错

if pcWire,ok := usb.(PCWire); ok == true {
//如果 usb 确实是 PCWire 的实例
fmt.Println("Disconnected: ", pcWire.name)
}
}
  • usb.(PCWire) 可以拿到相关的类型的实例,不过最好检查一下这个方法是否运行成功(可能拿到的是nil)
  • type assertion: 实例.(类型) 类型检查了。

全兼容的接口: 一个接口里面啥方法也不定义。那么等于说它能兼容所有的对象(所有对象都会实现这些空方法)

usb USB可以兼容所有实现该 USB 接口的实例,如果形参是 empty interface{} 那么这货就可以包容所有的类型啦

fmt.Println 就是这么定义的。

1
func Println(a ...interface{}) (n int, err error)

空接口 interface{} 其实就可以把它当成一个类型来用,通常也可以 type empty interface {}``,然后用 empty 即可。

其次,接口可以和嵌套结合:

为什么嵌入?代替面向对象的 继承 呗。

  • 接口可以内嵌接口 (相当于把公共方法提取到公共接口里)
  • 接口可以嵌入到其他类型中,比如 struct (意思是说,我有这样的实例作为字段)

内嵌定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//定义通用接口标准, 就一个连接方法,是个接口都能连接。
type Standard interface {
connect()
disconnect()
}

//其他具体接口本来应该继承它获取连接方法,但这里采用内嵌&组合
type USB2 interface {
getName() string
/*connect()
disconnect()*/
Standard //这就是内嵌定义了
}

type USB3 interface {
getName() string
highwayTransport()
Standard //内嵌标准接口
}

至于相关类型,实现具体接口,直接实现即可。

本质上还是会把内嵌接口下放,所以如果是多个内嵌,也会存在二义性问题

运行试试: (在上一个案例上抽取 Standard 接口)

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "fmt"

type USB interface {
getName() string //定义一个方法,参数为空,返回string类型
/*抽取到 Standard 接口
connect()
disconnect() */
Standard
}

type Standard interface {
connect()
disconnect()
}


type PCWire struct {
name string
}

func main() {
var usb USB
usb = PCWire{"PC 数据线"} //usb的运行时类型是 struct PCWire
str := usb.getName()
fmt.Println(str) //PC 数据线

//一般的用法,不用定义接口实例,直接指定实现接口的类型实例
usb2 := PCWire{"劣质 PC 数据线"}
usb2.connect()
usb2.disconnect()

usb3 := PCWire{"高级 PC 数据线"}
(*PCWire).connect(&usb3)
(*PCWire).disconnect(&usb3)


}

//实现了usb意味着三个方法都要实现

//receiver 可以指定对象或者指针
func (wire PCWire) getName() string {
return wire.name
}

func (wire PCWire) connect() {
fmt.Println(wire.name + "已经连接 USB")
}

func (wire PCWire) disconnect() {
fmt.Println(wire.name + "已经断开USB")
}

如果两个内嵌的接口有同样的方法呢?–不能处理。

共有方法问题

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
35
36
37
38
39
40
41
42
package main

import "fmt"

type A interface {
getName() string
xxx()
yyy()
}

type B interface {
getName() string //返回值不能做作为区分
zzz()
ssss()
}

/*duplicate method getName*/
type C1 interface {
A
B
}

type CC1 struct {
name string
}

func main() {
//var c C1
c := CC1{"ccc"}
fmt.Println(c.getName()) //编译报错:ambiguous selector c.getName
}

func (c CC1) getName() string{
return c.name
}

/* type C2 interface {
A
B
name() string
}
*/

和同为Golang的朋友讨论了一下,初步结论,go的设计团队没有在语言层面上解决这个问题,只能做一些检查。

还有一个问题:

1
usb := PCWire{"数据线"}

其实这里拿到的实例 usb 是按值拷贝的,即通过 usb 修改的字段其实是修改的副本,而接口里面初始化的那份则保持不变。

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
35
36
37
38
39
40
package main

import "fmt"

type USB interface {
printName()
}

type PCWire struct {
name string
}

func main() {
//var wire USB
//接口 = 对象, 此处就发生了拷贝,即 wire 其实 struct PCWire 实例的副本
wire := PCWire{"电脑数据线"}
var usb USB //接口内部保存着对于实例的引用,所以拿到该部分引用(原始对象)
usb = USB(wire) //抽象接口可以兼容,拿到接口所存储的实例的指针

wire.printName() //副本打印 "电脑数据线"
usb.printName() //接口拿到的打印 "电脑数据线"

//修改 wire 这个副本的内容
wire.name = "数据线(改)"
//当然通过 usb.name 修改肯定不行啦,接口没有这个字段

wire.printName() //副本的改变了: 数据线(改)
usb.printName() //接口内存储的指针则没有变: 电脑数据线
}

func (wire PCWire) printName() {
fmt.Println(wire.name)
}

/*
电脑数据线
电脑数据线
数据线(改)
电脑数据线
*/

接口对象如果指向某个对象,即使该对象是一个空指针,那么该接口对象也不空。

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

import "fmt"

type empty interface {}

type noempty interface {
hello()
}

func main() {
var em empty
fmt.Println(em == nil) //true

var no noempty
fmt.Println(no == nil) //true

var p *int = nil
em = p //只要接口收纳了对象, 它就不为空
fmt.Println(em == nil) //false
}
  • 接口对象为空的条件: 接口没有容纳对象。(即接口内部没有指向对象)

type_switch

如果判断一个实例的运行时类型,即到底是实现该接口的哪个实例?

上面给了一个方法, type asserting 检查中的一种:

1
2
3
4
if instance, ok := usb.(PCWire); ok {
//说明 usb 确实是 PCWire的实例
fmt.Println(instance.name); //可以直接拿到 instance 实例。
}

但是 现在有 N 个 struct都实现了这个 USB接口,那么这种ok pattern肯定是行不通的。

就引入了 type switch 更加高效的判断:

1
2
3
4
5
switch v := usb.(type) { //注意这里写入的是type关键字
case PCWire: /* ur code here */
case PhoneWire: /* ur code here */
...
}

和 ok pattern 类似, usb.(type)拿到的 v 就是运行时实例的类型,让它和case去匹配即可。

也可以各个 case 写成 PCWire == usb.(type),写 type 关键字而不是具体的类型可以让系统去确定。
两种写法没有本质的区别,但是上面那种用局部变量 v 更加简洁。

最后接口这里,我建议从抽象-具体的角度去理解;

(从超集,子集的角度也行,不过不直观?)

不管是 ok pattern 还是 type switch都表明,其实Go的接口完全可以面向接口编程,即支持运行时多态, 形参只管写接口类型即可。(这种动多态不借助继承,而直接用的接口方法实现达到目的, 有点点儿 Java 的意思)

强烈建议阅读《我为什么不喜欢Go语言式的接口》,这里槽点多多;请接着往下看。

方法集问题

事实证明 1.9 版本的 Go 已经可以处理好 receiver 的自动转换问题了.

以前说结构的时候,说过这样的问题:

1
2
3
4
5
6
7
type person struct {
name string
age int
}

instance := person{"bob", 20} //拿到副本
ptr := &person{"bob", 20} //直接拿到指针

那么之后通过 instanceptr 都可以去访问字段,或者访问方法,直接用就好了。

1
2
3
4
5
6
7
//访问字段
instance.name = "xxx"
ptr.name = "xxx"

//访问方法
instance.printName()
ptr.printName()

这里printName()方法会根据传入的是副本对象还是指针,做自动的转换。(无论 printName方法的 receiver 是 p *person,还是p person, 都能自动转换并处理)

但是如果通过接口调用,即把相关实例赋值给接口对象,通过接口调用接口的方法,那么这个时候,不会做自动转换。

  • 如果这个接口方法的 receiver 是指针,那么只能通过指针对象调用
  • 如果这个接口方法的 receiver 是值对象,那么可以通过指针或者值调用

下面是一个综合案例:

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
35
package main

//证明一下 receiver 不能自动转换

import "fmt"

type person struct {
name string
age int
}

func main() {
pp0 := person{"bob",20}
pp1 := &person{"bob", 20}

//访问自动,自动转换 ok
pp0.age++
pp1.age++

//访问方法自动转换 ok ( 调用 printPerson2() 把 printPerson1() 注释掉 )
/* pp0.printPerson2()
pp1.printPerson2() */

//调用 printPerson1() 把 printPerson2() 注释掉
pp0.printPerson1()
pp1.printPerson1()
}

func (p person)printPerson1() {
fmt.Println(p.name, p.age)
}

/* func (p *person)printPerson2() {
fmt.Println(p.name, p.age)
} */

上面充分证明了:

  • 对于结构来的方法的receiver,字段等,指针/实例可以完成自动转换 (但是传指针和值效果不一样)

然后,如果是 interface 的方法,指针和实例之间是不能相互转换的?(1.4版本的Go不行,1.9版本的可以么?验证一下)

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
35
36
package main

//证明一下 receiver 不能自动转换

import "fmt"

type print interface {
printPerson1()
printPerson2()
}

type person struct {
name string
age int
}

func main() {
//这里pp0,pp1分别是接口类型副本,指针
pp0 := person{"bob",20}
pp1 := &person{"bob", 20}

pp0.printPerson1()
pp0.printPerson2()

pp1.printPerson1()
pp1.printPerson2()
}
//该接口方法,实例和指针均可调用,自动转换ok
func (p person)printPerson1() {
fmt.Println(p.name, p.age)
}

//该接口方法,只能指针调用,实例调用失败----事实证明可以!!!
func (p *person)printPerson2() {
fmt.Println(p.name, p.age)
}

事实证明,不管是接口方法还是实例方法(普通的结构成员方法),receiver都可以完成自动转换

反射

貌似最早接触反射的时候,是在Java里面根据配置文件里面配置的 domain 类生成 pojo 对象容纳数据。

不过一般是写框架的人用反射比较多, 应用产品开发人员,业务开发人员用的少

作为内省的一种手段,反射提供了 抽象->具体 的实现,并且和编译类型无关。

Golang 里面,反射主要是 Typeof 和 Valueof,但是注意一下反射出来的原类型真实的原类型可能是一样的,但是由于反射中API的设计,一般知道是某个原始类型,但是调用的时候也要去写”从值、实例得到反射类型”的代码。这里代码开始不好理解,得反复写.

简单代码: 从实例获取它的运行时类型,并反射出属性,方法

首先注意一下:外部包能够反射的,一般都是能够 expoted 的字段和方法;如果都是小写开头,会得到这样的错误:

cannot return value obtained from unexported field or method

例如下面的代码:

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
35
36
37
38
39
40
41
package main

import "fmt"
import "reflect"

type User struct {
id int
name string
age int
}

type empty interface{}

func (usr User) print() {
fmt.Printf(usr.name, usr.id, usr.age)
}

func main() {
uu := User {1,"bob",20}
uu.info(uu)
}

//info 方法内部用到了反射
func (usr User)info(obj empty) {
//拿到类型
t := reflect.TypeOf(obj) // t 是 Type接口的实例副本
fmt.Println("Type:", t.Name()) //结构类型是有方法的,所以可以调用 Name()

//拿到值
v := reflect.ValueOf(obj) //这里拿到的结构 (而不是字段)
//字段循环取 (字段的类型、名字可以通过 t 获取;但是 value 只能通过 v 获取)
for i := 0; i < t.NumField(); i++ {
//拿到字段的名称,类型 (通过t)
field := t.Field(i) //拿到字段对象 (可以查看字段的类型,名称;但是拿不到值)
fmt.Print(field.Name," ", field.Type," ") //字段没有方法,直接用属性

//拿到字段的值
val := v.Field(i).Interface() //拿到 v当前值
fmt.Println(val)
}
}

OK, 我现在把字段方法的定义全部改成大写开头,但是!info方法的实现全部不用改~

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import "fmt"
import "reflect"

type User struct {
Id int
Name string
Age int
}

type empty interface{}

func (usr User) Print() {
fmt.Printf(usr.Name, usr.Id, usr.Age)
}

func main() {
uu := User {1,"bob",20}
uu.Info(uu)
}

//info 方法内部用到了反射
func (usr User)Info(obj empty) {
//拿到类型
t := reflect.TypeOf(obj) // t 是 Type接口的实例副本
fmt.Println("Type:", t.Name()) //结构类型是有方法的,所以可以调用 Name()

//拿到值
v := reflect.ValueOf(obj) //这里拿到的结构 (而不是字段)
//字段循环取 (字段的类型、名字可以通过 t 获取;但是 value 只能通过 v 获取)
for i := 0; i < t.NumField(); i++ {
//拿到字段的名称,类型 (通过t)
field := t.Field(i) //拿到字段对象 (可以查看字段的类型,名称;但是拿不到值)
fmt.Print(field.Name," ", field.Type," ") //字段没有方法,直接用属性

//拿到字段的值
val := v.Field(i).Interface() //拿到 v当前值
fmt.Println(val)
}
}

/*
Type: User
Id int 1
Name string bob
Age int 20
*/

也就是说,反射的代码,代码中根本不涉及原始代码的任何字符;也就不会因为原始类型修改,导致反射的代码编译不过。反射就好像是站在了编译器的角度去理解程序,而不是我们的角度(上帝视角)。

还有一段代码可以补充在后面,调用相关的方法:

1
2
3
4
5
//调用 obj 的方法 (可能有多个方法,所以也是循环)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type) //Type表示方法的签名 //Print: func(main.User)
}

看到的是 receiver 就是函数的第一个参数。

关于入参,如果传进来的是指针则不太好办,一般用 kind 做一下检查

1
2
3
4
5
6
7
8
9
10
11
12
func (usr User)Info(obj empty) {
//拿到类型
t := reflect.TypeOf(obj) // t 是 Type接口的实例副本
fmt.Println("Type:", t.Name()) //结构类型是有方法的,所以可以调用 Name()

if kind := t.Kind(); kind != reflect.Struct {
fmt.Println("传入反射函数的参数不是实例类型,您是不是传入了指针?")
return
}

/*下面代码,获取类型,值部分一样*/
}
  • 如果有嵌入(匿名的和有名的)字段怎么办?
  • 怎么样调用结构的方法(有参数的和无参数的)

嵌入结构: (嵌入结构当做外层结构的一个字段来解析)

  • 操作类型信息
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
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
"reflect"
)


type User struct {
Id int
Name string
Age int
}

type Manager struct {
User //只写了类型, 相当于匿名字段
title string
}

func main() {
m := Manager{User{1, "bob", 30}, "manager"}

//反射代码 - 对类型操作
typeManger := reflect.TypeOf(m)

//Field 拿到完整的匿名结构
fmt.Printf("%v\n", typeManger.Field(0))
fmt.Printf("%#v\n", typeManger.Field(0)) //输出完整结构信息

//FieldByIndex 拿到匿名结构内的所有字段, 以 slice 索引的形式
//直接拿到 manager, 相当于 Field(0)
fmt.Printf("%v\n", typeManger.FieldByIndex([]int{0}))
//取得 user.id
fmt.Printf("%v\n", typeManger.FieldByIndex([]int{0, 0})) //直接传递的 slice 要指定两级索引值
//相对于 Manager 而言 User 是第0个,相对于 User 而言 Id 是第0个。
//取得 user.name
fmt.Printf("%v\n", typeManger.FieldByIndex([]int{0, 1}))

//取得 manager.title
fmt.Printf("%v\n", typeManger.FieldByIndex([]int{1}))

//FieldByName, 同样用于取得 manager.title
titleName,_ := typeManger.FieldByName("title") //字段名字如果输入错误, 返回为空
fmt.Printf("%v\n", titleName)
}

总结起来就是: 操作类型,先拿到反射对象,然后从反射对象入手找Field.

  • 操作字段值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"
import "reflect"

func main() {
x := 99
fmt.Println(x)

//先获得值得反射对象
val := reflect.ValueOf(&x) //必须传递指针,因为下面要修改原值
val.Elem().SetInt(100)
//btw: Elem returns the value that the interface v contains

fmt.Println(x)
}

复杂一点的,操作结构的字段,也是类似的:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func main() {
u := &User{0, "bob", 20}
fmt.Println(*u)

Modify(u) //传入指针
fmt.Println(*u)
}


func Modify(o interface{}) {

//先拿到反射对象 reflect.Value 类型
valObj := reflect.ValueOf(o)
//var val Value //没必要这么定义,valObj本身类型就是 Value

//传入空接口一定要检查入参
if valObj.Kind() != reflect.Ptr || !valObj.Elem().CanSet() {
fmt.Println("传入不是指针或者不能改修原结构, 下面操作无法完成,直接退出")
return
} else {
valObj = valObj.Elem() //类型都是 Value 可以直接赋值
}

/* if age := valObj.FieldByName("Age"); age.Kind() ==reflect.Int {
age.SetInt(21)
} */

age := valObj.FieldByName("Age")

if(!age.IsValid()) { //没有找到返回的是 []Value{}
fmt.Println("没有找到相关的字段")
return
} //实际上没有找到字段去设置也是不成功的
if age.Kind() == reflect.Int {
age.SetInt(22)
}

}

调用结构的方法: (方法必须是 Exported 的)

调用结构的方法,不必要去分辨方法的类型(即原型或者说签名),直接拿到方法的 Value 进行调用即可。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"reflect"
)

type User struct {
id int
name string
age int
}

//给它添加一个方法

func (u *User) SetUserAge(age int) {
//fmt.Println("param age = ", age)
u.age = age
}

func (u User) Print() {
fmt.Println(u.name, " : ", u.age)
}

func main() {
u := &User{1, "bob", 20}
fmt.Println(u.age) //20
u.SetUserAge(21)
fmt.Println(u.age) //21 正常调用 ok

//反射调用 (先拿到反射 Value 对象)
valObj := reflect.ValueOf(u) //传入的是指针

/*应该做一些检查*/

//拿到方法
m := valObj.MethodByName("SetUserAge")
//设置参数
args := []reflect.Value{reflect.ValueOf(22)} //int类型的 Value

//传入参数,反射调用
m.Call(args)
fmt.Println(u.age) //22

m1 := valObj.MethodByName("Print")
m1.Call([]reflect.Value{}) //无参数调用
}
  • 调用方法时,方法需要的参数,也是 reflect.Value 类型, 即[]Value{}成员要是 Value 类型的
  • 无参数,多参数时,反射调用类似
  • 正规的做法,也要判断方法是否可以被调用,传入的参数是否是指针等

方法也好,字段也好,在反射操作中,一定分开处理 Type 和 Value

(因为语言的设计者在设计 API 的时候就是这么设计的)

文章目录
  1. 1. Main函数
  2. 2. import别名
  3. 3. 可见性规则
  4. 4. 组定义
  5. 5. 基本类型
  6. 6. 其他类型
  7. 7. 零值
  8. 8. 隐式转换
  9. 9. 类型转换
  10. 10. 空白符号
  11. 11. 常量
  12. 12. 枚举
  13. 13. 运算符
  14. 14. 递增递减
  15. 15. 指针
  16. 16. if条件控制语句
  17. 17. switch条件控制语句
  18. 18. 循环语句
  19. 19. 跳转语句
  20. 20. 数组
  21. 21. new关键字
  22. 22. slice切片
  23. 23. map
  24. 24. 函数
  25. 25. defer关键字
  26. 26. panic/recover模式
  27. 27. struct结构
  28. 28. 类型方法
  29. 29. 接口
  30. 30. type_switch
  31. 31. 方法集问题
  32. 32. 反射
|