学习 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 mainimport "fmt" const PI = 3.14 type type_name int type struct_name struct { name string } type interface_name interface { } func main () { fmt.Println("你好世界, hello 中国" ) }
import别名
import 的时候,可以使用别名 import alias_name real_name
, 或者使用.
代替(不建议, 容易混淆)
1 2 3 4 5 6 7 package mainimport 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 = 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 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 mainimport "fmt" func main () { var cnt int fmt.Println(cnt) var bb []byte fmt.Println(bb) var intArr [3 ]int fmt.Println(intArr) var flag bool fmt.Println(flag) }
隐式转换 Go语言没有隐式转换,只有强制类型转换。
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var b int b = 1 b = 1.1 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 mainimport "fmt" func main () { var a rune = 10 var b int b = int (a) fmt.Println(b) }
非bool 和 bool 之间不兼容!(数字和逻辑之间不兼容)
string 和 其他类型呢 ?
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { a := 65 str := string (a) fmt.Println(str) }
可能它默认你需要的是这个数字表示的字符,其他类型和 string 转换借助 strconv
包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "strconv" "fmt" ) func main () { var a = 65 str := strconv.Itoa(a) fmt.Println(str) }
空白符号
一个下划线 _
用来赋值忽略
例子:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { a, _, c := 1 , 2 , 3 fmt.Println(a) fmt.Println(c) }
这么直白的看上去,似乎有点儿傻;但是一般用于函数多返回值
的情景。
常量
首先说一下,下面要讲解的常量&枚举,应该使用大写 !
这里使用小写主要为了演示用。(同时,使用小写字母开头或者下划线开头,禁止其他包访问)
常量的值,在编译时就已经确定了。
定义格式和变量类似,除了 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 mainimport "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) fmt.Println(a) fmt.Println(b) fmt.Println(e) fmt.Println(f) }
常量一定要加 const 修饰
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" var str = "xxxx" func main () { const length = len (str) 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 mainimport "fmt" const ( a = "A" b c = iota d ) func main () { fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) num := 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 mainimport "fmt" func main () { fmt.Println(!true ) fmt.Print("^一元运算: " ) fmt.Println(^1 ) fmt.Println(1 &0 ) fmt.Println(1 |0 ) fmt.Println(1 ^0 ) fmt.Println(0 ^1 ) fmt.Println(1 ^1 ) fmt.Println(0 ^0 ) fmt.Println("-----special-----" ) fmt.Println(1 &^0 ) fmt.Println(0 &^1 ) fmt.Println(0 &^0 ) fmt.Println(0 &^1 ) fmt.Println(1 &^1 ) fmt.Println("-----special-----" ) fmt.Println(1 <<10 ) fmt.Println(8 >>3 ) fmt.Println(false && true ) fmt.Println(true && false ) fmt.Println(false || true ) }
关于位移运算,这里有一个经典的应用:
1 2 3 4 5 6 7 8 9 10 11 const ( _ = iota KB = 1 << (10 * iota ) 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 mainimport "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 mainimport "fmt" func main () { a := 1 a++ b := a fmt.Println(b) }
指针 这里指针运算比较滑稽,不支持 ->
运算,而是统一采用 .
运算来操作。
基本用法还是类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" func main () { a := 1 b := &a fmt.Println(b); fmt.Println(*b); var p *int fmt.Println(p) }
有没有指针运算 ?
貌似也没有。
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 mainimport "fmt" func main () { a := 1 if a > 0 { fmt.Println("a 可以继续参加运算." ); } if b:=1 ; a == b { fmt.Println("a 和 b 相等." ); } }
注意,局部变量还是会隐藏范围更大的变量。
switch条件控制语句 switch 语句被增强了 : (带有条件判断)
switch 支持一个初始化表达式(可以是并行方式,即同时按顺序初始化多个,最右侧要写分号)
switch 后面可以不写值
case 可以是单一值,多值或者 condition条件
不用写 break 语句,执行完一个 case 会自动跳出去(跳出整个 switch)
如果想接着执行,那么要写 fallgroup
大致就这三种形式:
相关 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 mainimport "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部分齐全)
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 mainimport "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 true {}
。
长得像while的for循环 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { a := 1 for a > 0 { fmt.Println("循环中..." ); a-- } fmt.Println("循环结束了..." ) }
完整的for循环 :
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var sum int for i :=1 ; i <= 100 ; i++ { sum += i } fmt.Println(sum) }
总结下来,非常灵活;需不需要条件,完全自己看着办。
还有一个 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 mainimport "fmt" func main () { arr := [5 ]int {1 ,2 ,3 ,4 ,5 } 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 mainimport "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 mainimport "fmt" func main () { var a [2 ]int var b [2 ]int a = b fmt.Println(a); aa := [3 ]int {1 } fmt.Println(aa) bb := [5 ]int {3 :1 } 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 mainimport "fmt" func main () { a := [10 ]int {2 :1 , 3 :1 } var p *[10 ]int 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 mainimport "fmt" func main () { x, y := 1 , 2 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 mainimport "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) fmt.Println(a==c) aa := [...]string {"hello" } bb := [...]string {"hello" } cc := [...]string {"hello" , "world" } fmt.Println(aa==bb) }
通过下标操作数组 :
1 2 3 4 5 6 7 8 9 10 package mainimport "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 mainimport "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 mainimport "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] { tmp := arr[i] arr[i] = arr[j] arr[j] = tmp } } } fmt.Println(arr) }
new关键字 可以通过 new 关键字拿到相关类型的地址。
比如:
1 2 3 4 5 6 7 8 9 10 11 package mainimport "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 mainimport "fmt" func main () { var sla []int fmt.Println(sla) arr := [10 ]int {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 } fmt.Println(arr) sli := arr[5 :] 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 mainimport "fmt" func main () { slice := make ([]int , 10 , 15 ) fmt.Println(slice) fmt.Println(len (slice), cap (slice)) }
Rslice : 简单说,就是截取现有的 slice,从一个 slice 中获得一个新的 slice。
每一次取 slice 索引就又重新规划一次 (从0开始)。
新的 slice 可能长度有限,但是容量是截取的开始到原来 slice 的末尾(底层还是那个数组)
新的 slice 的最大索引不能超过原 slice 的 cap (如果按索引取值越界,会引发错误;但不会导致底层数组重新分配)
由于都共享底层数组,那么一个slice的改变可能会引起另外一个slice值改变
这里 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 mainimport "fmt" func main () { oldSlice := []byte {'a' ,'b' ,'c' ,'d' ,'e' } sliceA := oldSlice[0 :3 ] fmt.Println(string (sliceA)) sliceB := oldSlice[3 :5 ] fmt.Println(string (sliceB)) sliceC := sliceB[1 :] fmt.Println(string (sliceC)) fmt.Println(len (oldSlice), cap (oldSlice)) fmt.Println(len (sliceA), cap (sliceA)) fmt.Println(len (sliceB), cap (sliceB)) fmt.Println(len (sliceC), cap (sliceC)) }
SliceAppend :
使用 append 函数
可以在尾部追加元素
可以在尾部追加slice
(追加完成了,如果没有超过cap,那么返回原来slice,否则返回重新分配的slice,同时拷贝元素) (重新分配,一般是原来的 cap 翻倍)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { s1 := make ([]int , 3 , 4 ) fmt.Printf("%v, %p\n" , s1, s1) s1 = append (s1, 1 , 2 ) 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 mainimport "fmt" func main () { arr := []int {1 ,2 ,3 } s1 := arr[0 :2 ] s2 := arr[1 :3 ] fmt.Println(s1) fmt.Println(s2) s2[0 ] = 4 fmt.Println(s1) fmt.Println(s2) fmt.Println(cap (s2)) s2 = append (s2, 2 , 3 , 4 ) fmt.Println(s2) s2[0 ] = 9 fmt.Println(s1) fmt.Println(s2) }
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 mainimport "fmt" func main () { s1 := []int {1 ,2 ,3 ,4 } s2 := []int {5 ,6 ,7 } copy (s2, s1) fmt.Println(s2) copy (s2, s1[1 :]) 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 mainimport "fmt" func main () { var m1 map [int ]string m1 = map [int ]string {} fmt.Println(m1) var m2 map [int ]string = make (map [int ]string , 3 ) fmt.Println(m2) m3 := make (map [int ]string ) fmt.Println(m3) m1[1 ] = "Ok" fmt.Println(m1[1 ]) m1[1 ]="Cancel" fmt.Println(m1[1 ]) 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 mainimport "fmt" func main () { complex_map := make (map [int ]map [int ]string ) str, ok := complex_map[1 ][1 ] if !ok { 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 mainimport "fmt" func main () { sli := make ([]map [int ]string , 3 , 4 ) for _,value := range sli { value = make (map [int ]string , 1 ) value[1 ]="OK" fmt.Println(value) } fmt.Println(sli) 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 mainimport "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) } i := 0 sli := make ([]int , 5 , 5 ) for k := range m1 { sli[i] = k i++ } fmt.Println(sli) sort.Ints(sli) 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 mainimport "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 } 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 mainimport "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 mainimport "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 ) { 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 mainimport "fmt" func main () { A() } func A () { a := AA a() } func AA () { fmt.Println("调用AA" ) }
匿名函数 : (就是定义的时候不给名字,其他一切如常)
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { a := func () { 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 mainimport "fmt" func main () { codeBlock := closure(1 ) result1 := codeBlock(0 ) result2 := codeBlock(2 ) fmt.Println("result1 = " , result1) fmt.Println("result2 = " , result2) } func closure (x int ) 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 mainimport "fmt" func main () { x := 1 fmt.Printf("main函数: x地址 = %p\n" , &x) codeBlock := closure(x) codeBlock(3.14 ) } func closure (x int ) func (float32 ) float32 { fmt.Printf("闭包内部: x地址 = %p\n" , &x) return func (y float32 ) float32 { fmt.Printf("Code Block内部: x地址 = %p\n" , &x) return y } }
我试试按照指针来一波: (这回捕获的不再是值拷贝了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { x := 1 fmt.Printf("main函数: x地址 = %p\n" , &x) codeBlock := closure(&x) codeBlock(3.14 ) } func closure (x *int ) func (float32 ) float32 { fmt.Printf("闭包内部: x地址 = %p\n" , x) return func (y float32 ) float32 { fmt.Printf("Code Block内部: x地址 = %p\n" , x) 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 mainimport "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 mainimport "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) }
先看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 mainimport "fmt" func main () { b := 1 b = 2 defer subFunc(b) b = 3 fmt.Printf("outer b: %v, %p\n" , b, &b) } func subFunc (b int ) { fmt.Printf("defer b: %v, %p\n" , b, &b) }
但是内置函数有点奇葩 :
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { a := 1 a = 2 defer fmt.Printf("defer a: %v,%p\n" , a, &a) a = 3 fmt.Printf("outer a: %v,%p\n" , a, &a) }
原则上:
是同一个地址的话,应该取函数运行结束时的值(比如:匿名函数直接拿到&使用 的就是同一个地址,所以值是末尾执行完毕的值)
副本地址的话,就是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 mainimport "fmt" func main () { defer fmt.Println("先声明" ) A() B() C() defer fmt.Println("后声明" ) } func A () { fmt.Println("func A" ) } func B () { fmt.Println("func B" ) 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 mainimport "fmt" func main () { 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 ("发生严重错误了" ) } 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 mainimport "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 mainimport "fmt" type person struct { name string age int } func main () { p1 := person { name: "bob" , age: 20 , } setPerson(&p1) fmt.Println("outer: " , p1) p2 := &person { name: "mike" , age:30 , } setPerson(p2) fmt.Println("outer: " , *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 mainimport "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 mainimport "fmt" type person struct { name string age int contact struct { city, dic string } } func main () { pp := & person{} fmt.Println(*pp) }
定义完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 mainimport "fmt" type person struct { name string age int contact struct { city, dic string } } func main () { pp1 := & person{name:"joe" , age:19 } fmt.Println(*pp1) pp1.contact.city = "上海" pp1.contact.dic = "黄浦区" fmt.Println(*pp1) fmt.Println("--------------" ) }
也就是说,如果在结构里定义了匿名结构,初始化只能在外部进行。(编译器能力有限, 识别不了你的花样)
如果是嵌入的有名结构,则可以定义实例的时候就指明:
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 mainimport "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 } 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 mainimport "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{} fmt.Println(cc1) fmt.Println(cc1.A.name) cc2 := C2{} fmt.Println(cc2) fmt.Println(cc2.name) }
最高级别如果不存在某个字段,那么找内部嵌入的strut的字段
匿名字段 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" type person struct { string int } func main () { pp := &person {"mike" , 20 } fmt.Println(*pp) }
按规矩做事情,还是不要搞匿名字段比较好。
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 mainimport "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) pp1.age += 1 fmt.Println("pp0 == pp1, " , pp0 == pp1) }
类型方法 给 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 mainimport "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) } func (p person) printPerson1 () { p.age ++ fmt.Println("inner print1" , p.name, p.age) } func (p *person) printPerons2 () { p.age ++ fmt.Println("inner print2" , p.name, p.age) }
如果指定 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 mainimport "fmt" type XX int func main () { var x XX x.printInt() } func (p *XX) printInt () { *p = *p + 1 fmt.Println(*p) }
一般基本类型或者不能直接指定方法的类型,都要借助类型别名 。(相当于绕过语法限制)
类型别名指定的方法只能通过别名的实例调用,而不能被原来的类型实例直接调用
类型别名和原来的类型混合计算的时候,要把原来的类型强转成类型别名的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" type XX int func main () { var x XX x.setInt(3 ) } func (p *XX) setInt (num int ) { *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 mainimport "fmt" type XX int func main () { var x XX (*XX).printInt(&x) } func (p *XX) printInt () { *p = *p + 1 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 mainimport "fmt" type USB interface { getName() string connect() disconnect() } type PCWire struct { name string } func main () { var usb USB usb = PCWire{"PC 数据线" } str := usb.getName() fmt.Println(str) usb2 := PCWire{"劣质 PC 数据线" } usb2.connect() usb2.disconnect() usb3 := PCWire{"高级 PC 数据线" } (*PCWire).connect(&usb3) (*PCWire).disconnect(&usb3) } 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 func ensureInterface (usb USB) { fmt.Println("正常调用该方法,说明传入的实参是 usb 的实例" ) fmt.Println(usb.getName()) }
子类或者具体类型不能向抽象类型转换,不兼容。(抽象包容具体是ok的)
不能强制类型转换,但是我就是想从实例那儿拿到字段怎么办?判断一下运行时类型 type assertion .
1 2 3 4 5 6 7 8 9 10 func ensureInterface (usb USB) { if pcWire,ok := usb.(PCWire); ok == true { 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 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 mainimport "fmt" type USB interface { getName() string Standard } type Standard interface { connect() disconnect() } type PCWire struct { name string } func main () { var usb USB usb = PCWire{"PC 数据线" } str := usb.getName() fmt.Println(str) usb2 := PCWire{"劣质 PC 数据线" } usb2.connect() usb2.disconnect() usb3 := PCWire{"高级 PC 数据线" } (*PCWire).connect(&usb3) (*PCWire).disconnect(&usb3) } 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 mainimport "fmt" type A interface { getName() string xxx() yyy() } type B interface { getName() string zzz() ssss() } type C1 interface { A B } type CC1 struct { name string } func main () { c := CC1{"ccc" } fmt.Println(c.getName()) } func (c CC1) getName () string { return c.name }
和同为Golang的朋友讨论了一下,初步结论,go的设计团队没有在语言层面上解决这个问题,只能做一些检查。
还有一个问题:
其实这里拿到的实例 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 mainimport "fmt" type USB interface { printName() } type PCWire struct { name string } func main () { wire := PCWire{"电脑数据线" } var usb USB usb = USB(wire) wire.printName() usb.printName() wire.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 mainimport "fmt" type empty interface {}type noempty interface { hello() } func main () { var em empty fmt.Println(em == nil ) var no noempty fmt.Println(no == nil ) var p *int = nil em = p fmt.Println(em == nil ) }
接口对象为空的条件: 接口没有容纳对象。(即接口内部没有指向对象)
type_switch 如果判断一个实例的运行时类型,即到底是实现该接口的哪个实例?
上面给了一个方法, type asserting 检查中的一种:
1 2 3 4 if instance, ok := usb.(PCWire); ok { fmt.Println(instance.name); }
但是 现在有 N 个 struct都实现了这个 USB接口,那么这种ok pattern 肯定是行不通的。
就引入了 type switch
更加高效的判断:
1 2 3 4 5 switch v := usb.(type ) { case PCWire: case PhoneWire: ... }
和 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 }
那么之后通过 instance
和 ptr
都可以去访问字段,或者访问方法,直接用就好了。
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 mainimport "fmt" type person struct { name string age int } func main () { pp0 := person{"bob" ,20 } pp1 := &person{"bob" , 20 } pp0.age++ pp1.age++ pp0.printPerson1() pp1.printPerson1() } func (p person) printPerson1 () { 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 mainimport "fmt" type print interface { printPerson1() printPerson2() } type person struct { name string age int } func main () { pp0 := person{"bob" ,20 } pp1 := &person{"bob" , 20 } pp0.printPerson1() pp0.printPerson2() pp1.printPerson1() pp1.printPerson2() } 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 mainimport "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) } func (usr User) info (obj empty) { t := reflect.TypeOf(obj) fmt.Println("Type:" , t.Name()) v := reflect.ValueOf(obj) for i := 0 ; i < t.NumField(); i++ { field := t.Field(i) fmt.Print(field.Name," " , field.Type," " ) val := v.Field(i).Interface() 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 mainimport "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) } func (usr User) Info (obj empty) { t := reflect.TypeOf(obj) fmt.Println("Type:" , t.Name()) v := reflect.ValueOf(obj) for i := 0 ; i < t.NumField(); i++ { field := t.Field(i) fmt.Print(field.Name," " , field.Type," " ) val := v.Field(i).Interface() fmt.Println(val) } }
也就是说,反射的代码,代码中根本不涉及原始代码的任何字符;也就不会因为原始类型修改,导致反射的代码编译不过。反射就好像是站在了编译器 的角度去理解程序,而不是我们的角度(上帝视角)。
还有一段代码可以补充在后面,调用相关的方法:
1 2 3 4 5 for i := 0 ; i < t.NumMethod(); i++ { m := t.Method(i) fmt.Printf("%s: %v\n" , m.Name, m.Type) }
看到的是 receiver 就是函数的第一个参数。
关于入参,如果传进来的是指针则不太好办,一般用 kind 做一下检查
1 2 3 4 5 6 7 8 9 10 11 12 func (usr User) Info (obj empty) { t := reflect.TypeOf(obj) fmt.Println("Type:" , t.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 mainimport ( "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) fmt.Printf("%v\n" , typeManger.Field(0 )) fmt.Printf("%#v\n" , typeManger.Field(0 )) fmt.Printf("%v\n" , typeManger.FieldByIndex([]int {0 })) fmt.Printf("%v\n" , typeManger.FieldByIndex([]int {0 , 0 })) fmt.Printf("%v\n" , typeManger.FieldByIndex([]int {0 , 1 })) fmt.Printf("%v\n" , typeManger.FieldByIndex([]int {1 })) 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 mainimport "fmt" import "reflect" func main () { x := 99 fmt.Println(x) val := reflect.ValueOf(&x) val.Elem().SetInt(100 ) 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 mainimport ( "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 {}) { valObj := reflect.ValueOf(o) if valObj.Kind() != reflect.Ptr || !valObj.Elem().CanSet() { fmt.Println("传入不是指针或者不能改修原结构, 下面操作无法完成,直接退出" ) return } else { valObj = valObj.Elem() } age := valObj.FieldByName("Age" ) if (!age.IsValid()) { 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 mainimport ( "fmt" "reflect" ) type User struct { id int name string age int } func (u *User) SetUserAge (age int ) { u.age = age } func (u User) Print () { fmt.Println(u.name, " : " , u.age) } func main () { u := &User{1 , "bob" , 20 } fmt.Println(u.age) u.SetUserAge(21 ) fmt.Println(u.age) valObj := reflect.ValueOf(u) m := valObj.MethodByName("SetUserAge" ) args := []reflect.Value{reflect.ValueOf(22 )} m.Call(args) fmt.Println(u.age) m1 := valObj.MethodByName("Print" ) m1.Call([]reflect.Value{}) }
调用方法时,方法需要的参数,也是 reflect.Value 类型, 即[]Value{}
成员要是 Value 类型的
无参数,多参数时,反射调用类似
正规的做法,也要判断方法是否可以被调用,传入的参数是否是指针等
方法也好,字段也好,在反射操作中,一定分开处理 Type 和 Value
(因为语言的设计者在设计 API 的时候就是这么设计的 )