go语言入门
title: go语言入门
date: 2019-02-12 22:03:27
前言
因项目需要和个人喜好,决定系统入门go语言。
go是由Google开发、开源、强类型的编译型语言。与c语言类似,不同的是,go中每行语句结束不用加 ;
:-)
本笔记主要参考 《Go语言实战》 。
后加:本文所基于的GO语言版本较低(1.10),当时还并未支持 Go Modules
。
一、Hello world
编写hello.go
文件:
package main // 程序入口包
import (
"fmt"
)
// 程序入口函数
func main() {
fmt.Println("Hello world")
}
在命令行中输入:go run hello.go
二、基础语言
1. 变量
1.1. 基本变量类型
// 布尔型
bool
// 字符串型
string
// 整型
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
// byte型
byte // uint8
// 表示一个 Unicode 码点
rune // int32 的别名
// 浮点型
float32 float64
// 复数型
complex64 complex128
1.2. 变量声明
特性:
- 使用
var
关键字声明(像js),变量类型在变量名后 - 短变量声明 (像python)
- 使用
()
一次声明多个变量 - 若声明的变量没被使用,会报错
// 一般声明
var v_name1 int
var v_name2 = 1 // 根据值自行判别变量类型
// 短变量声明。不能用于声明全局变量
v_name3 := "hello"
// 多变量声明
var vname1, vname2, vname3 int
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
)
// 指针变量声明
var p *int
v_p := &v_name1
*v_p = 233
注:和c
类似,go也分 全局变量(函数外、包内) 和 局部变量(函数内/控制语句内)
1.3. 变量零值
变量声明时没有赋予初始值,则默认被赋予零值。
- 布尔型零值:
false
- 字符串型零值:
""
- 数值型零值:
0
- 指针型零值:
nil
1.4. 强制类型转换
表达式T(v)
将值v
转换为类型T
。
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
1.5. 类型推导
当不指定数据类型时,系统会自行推导变量类型。如下:
var i int
j := i // j 也是一个 int
// 初始值为常量,则取决于常量的精度
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
1.6. 变量输出
使用fmt
包中的函数:fmt.Printf
和fmt.Println
。
格式化输出fmt.Printf
== c语言中的printf
。%T
输出数据的类型,%v
输出任意数据的值,%p
输出地址数据,%d
输出整型数据,等等。
直接输出fmt.Println
== python中的print
。
2. 常量
用const
关键字常量。可以不指定常量的数据类型。
const b string = "abc"
const b = "abc"
const Pi = 3.14
3. 运算符
运算符与c
语言类似。具体如下,优先级从高到低:
分类 | 描述 | 关联性 | ||
---|---|---|---|---|
后缀 |
() [] -> . ++ --
|
左到右 | ||
一元 |
+ - ! ~ (type) * & sizeof()
|
右到左 | ||
乘法 |
* / %
|
左到右 | ||
加法 |
+ -
|
左到右 | ||
移位 |
<< >>
|
左到右 | ||
关系 |
< <= > >=
|
左到右 | ||
相等 |
== !=
|
左到右 | ||
按位AND | & |
左到右 | ||
按位XOR | ^ |
左到右 | ||
按位OR | ` | ` | 左到右 | |
逻辑AND | && |
左到右 | ||
逻辑OR | ` | ` | 左到右 | |
条件 | ?: |
右到左 | ||
分配 |
= += -= *= /= %= >>= <<= &= ^= ` |
=` | 右到左 | |
逗号 | , |
左到右 |
4. 语句
go中if
和for
语句不需要加()
;语句大括号{
不需要换行。
4.1. if语句
特性:
- 可以初始化变量,仅在
if
语句中使用。
// if...else...
if numA < 20 {
fmt.Printf("a小于20\n" );
} else {
fmt.Printf("numA 不小于 20\n" );
}
// if语句的分号前面,相当于初始化一个变量,仅在if语句内使用
if i := 30; numA < i {
fmt.Println(i)
}
4.2. switch语句
特性:
- case语句结束自动break
- case可以同时匹配多个值,如:
case v1,v2,v3:
- 匹配一个case成功后,可以使用
fallthrough
强制匹配下一个case -
switch
可以没有条件
var grade string
var marks int = 90
// case语句结束自动break。
// 可以同时case多个值,case v1,v2,v3:
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
fmt.Printf("你的等级是 %s\n", grade );
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
/*
输出:
2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true
*/
4.3. select语句
select
语句类似于switch
语句。
但是,区别在于:
- 每个case必须是一个通信操作(数据结构通道的操作),要么是发送要么是接收。
- select随机执行一个未堵塞的case。如果所有case都堵塞,它将等待,直到有case可以通行。
var c1, c2, c3 chan int
var i1, i2 int
// 随机执行一个case,若所有case都堵塞,则直到有case可以通行为止
select {
case i1 = <-c1:
fmt.Println("received ", i1, " from c1")
case c2 <- i2:
fmt.Println("sent ", i2, " to c2")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Println("received ", i3, " from c3")
} else {
fmt.Println("c3 is closed")
}
default:
fmt.Println("no communication")
}
/*
输出:
no communication
*/
4.4. for语句
特性:
-
for
只带条件判断相当于while
-
for
中使用range
关键字,可以遍历顺序结构,返回key
和value
for i := 0; i < 10; i++ {
fmt.Printf("i 的值为: %d\n", i)
if i > 5{
break
}
}
// 相当于c的while
for numA < numB {
numA++
fmt.Printf("numA 的值为: %d\n", numA)
}
numbers := [6]int{1, 2, 3, 5}
// range关键字,可以对 slice、map、数组、字符串等进行迭代
for key, value := range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", key, value)
}
5. 函数 引用-数据类型
go语言的一大“神奇”特点,就是喜欢把原本前面的东西放到后面,函数也不例外。
特性:
- 函数以关键字
func
进行声明 - 返回类型(和值),放在参数项的后面
- 允许先声明返回值
- 多值返回
-
defer
。defer
语句会将指定函数推迟到外层函数返回之后再执行。并且,被推迟的函数将被压入一个栈中。
//==========函数============
func add(x int, y int) int {
return x + y
}
// 先声明返回值,直接用return返回。适用于短函数
func subtract(x int, y int) (z int) {
z = x - y
return
}
// 返回多值
func swap(x, y string) (string, string) {
return y, x
}
/*
使用:
stringA, stringB = swap(stringA, stringB)
*/
// 传指针
func swap_p(x, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
// defer栈
func deferTest() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
/*
输出
counting
done
9
8
7
6
5
4
3
2
1
0
*/
另外,跟JavaScript
一样,go语言也有:
- 函数变量,函数也是属于一类数据类型。
- 函数做参。多态性的一种体现。
- 函数作为返回值(函数闭包)。得以使用函数的局部变量。
- 匿名函数
- 函数自调用
// 函数做参。函数参数,要指明函数参数的参数类型和返回值类型
func handleNum(num float64, fn func (float64) float64) {
fmt.Println("函数做参开始")
fmt.Println(fn(num))
fmt.Println("函数做参结束")
}
// 闭包函数(将函数作为返回值)
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main() {
// 函数变量
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
fmt.Println(getSquareRoot(9))
// 函数做参
handleNum(16, getSquareRoot)
// 函数闭包
nextNumber := getSequence() // nextNumber 为一个函数,函数 i 为 0
fmt.Println(nextNumber()) // 调用 nextNumber 函数,i 变量自增 1 并返回
fmt.Println(nextNumber())
fmt.Println(nextNumber())
nextNumber1 := getSequence() // 创建新的函数 nextNumber1,并查看结果
fmt.Println(nextNumber1()) // 输出 1
fmt.Println(nextNumber1()) // 输出 2
// 匿名函数和函数自调用
func (count int) {
fmt.Println("匿名函数开始")
for i := 0; i < count; i++ {
fmt.Println(i)
}
fmt.Println("匿名函数结束")
} (3)
}
输出:
函数做参开始
4
函数做参结束
1
2
3
1
2
匿名函数开始
0
1
2
匿名函数结束
6. 包
6.1. 包的特性
与java
相似,每个.go
文件开头需要用package
关键字声明文件所属于的包。并且,包的名字需要与目录名字相同。main
包除外。
例如,项目中有一个routers/
目录,routers/
目录下有一个router.go
文件,那router.go
文件开头必须声明所属于的包:
package routers
6.2. main包
每个程序(项目)都必须有一个main
包,编译器会根据main
包找到main()
函数,这是程序的入口函数。若没找到main()
函数,程序则不会执行。
6.3. 导入包
import
关键字用于导入一个外包。格式如下:
import "fmt"
或者导入多个包时:
import (
"fmt"
"math"
"net/http"
)
根据以上包名,编译器会依次在以下目录中查找:
1)GOROOT/src
,安装路径下的src
目录
2)GOPATH/src
,工作空间下的src
目录
3)若以上都没找到,且包路径中包含URL
,那么会从网上获取包,并保存到GOPATH/src
目录下。比如:
import "github.com/99MyCql/chatRoom/routers"
6.4. 命名导入
导入包的名字默认为包名,但如果出现重名情况,我们可以通过给包重新命名来化解。
import (
"fmt"
myfmt "mylib/fmt" // myfmt为该包的新名字
)
go语言中若导入了某包(会调用该包中的init()
函数),而又没使用该包,编译器则会报错。
解决这个问题可以使用空白标识符_
来重命名这个包,表明导入该包却不使用该包。如:
import "github.com/99MyCql/chatRoom/routers"
6.5. init()函数
一个包中,可以有一个或多个init()
函数(多个init()函数不能在同一个.go
文件中)。
init()
函数会在main()
函数执行前被调用。
每个被导入的包(不管有没有被使用),都会调用包中的所有init()
函数。通常,init()
函数被用来进行一些初始化操作。
使用空白标识符_
,可以让包中的init()
函数被调度使用,同时编译器不会因为包没被使用而报错。
6.6. 包中名的可见性(special)
在一个包内,所有文件的全局变量是共享的。
对于包外,以大写字母开头的全局变量和函数是公开的,以小写字母开头的是私有的。如:
-
fmt.Println()
是调用fmt
包中公开的Println()
函数。 -
fmt.Println(math.pi)
输出math
包中变量,会报错,因为该变量是私有的。
6.7. 使用另一个包中的变量和函数
通过 <package name>.<var/fun>
格式(跟C++使用类中变量函数相似),来使用另一个包中公开的变量和函数。如:
fmt.Println() // 使用 fmt 包中 Println() 函数
math.PI // 使用 math 包中 PI 变量
三、进阶数据结构
1. 指针 值-数据类型
与c语言指针类似,go指针指向对应类型的变量。
但不同的是:
-
go语言指针不能进行运算
-
指针变量的声明中,标识符
*
必须贴近变量类型,而不贴近变量名。如:var name *T
-
指针类型的零值为
nil
// 指针变量声明
var p *int
v_p := &v_name1
*v_p = 233
2. 数组 值-数据类型
go语言数组跟c语言的相似,但也有不同。
特性:
-
格式
var name [len]T
,如:var a [2]string
-
go语言的数组是一种数据类型,而且是一种值类型。即数组名是一个值,包含着整个数组的数据
-
需要编译器自己识别数组长度时,不能使
[]
中空闲,而必须使用[...]
-
指向数组的指针格式为:
var name *[len]T
,如:var arrp *[5]int
//====================数组=====================
// 变量 a 是一个值类型,而不是引用类型。包含着整个数组的数据
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
// 使用字面量声明数组
array1 := [5]int{10,20,30,40,50}
array2 := [...]int{1,2,3,4,5} // ... 可以使编译器根据元素数量,自动确定数组长度
fmt.Println(array1, array2)
// 数组元素类型为指针
array3 := [5]*int{0:new(int), 1:new(int)} // 用 下标:... 进行特定位置的初始化
*array3[0] = 10
*array3[1] = 20
fmt.Println(*array3[0])
fmt.Println(array3)
// 多维数组
var array4 [4][2]int // 4行2列的二维数组,即有4行,每行有2个int
// 指向数组变量的指针
arrP := &a
fmt.Printf("a 's type is %T\n", a)
fmt.Printf("arrp 's type is %T\n", arrP)
(*arrP)[1] = "Today" // arrP[1] = "Today" 也可以
fmt.Println(*arrP)
// new 指向数组的指针
var arrp *[5]int // 此时为nil
fmt.Println(arrp) // 输出 nil
arrp = new([5]int) // 分配相应的数组空间,并返回指针
fmt.Println(arrp) // 输出 &[0 0 0 0 0]
// 与c语言的不同,go的数组名是值而不是指针。以下按照c语言的思路,在go中是错误的
// var p *int
// p = a
// 数组是值类型而不是引用类型的示例
arrA := [2]int{1,2}
arrB := arrA // 相当于赋值了整个数组的值
arrB[1] = 3
fmt.Println(arrA, arrB) // 输出 [1 2] [1 3]
输出:
Hello World
[Hello World]
[10 20 30 40 50] [1 2 3 4 5]
10
[0xc42008a018 0xc42008a030 <nil> <nil> <nil>]
[[0 20] [0 0] [0 60] [0 0]]
a 's type is [2]string
arrp 's type is *[2]string
[Hello Today]
<nil>
&[0 0 0 0 0]
[1 2] [1 3]
注意:
由于在go语言中,数组类型是值类型而不是引用类型。所以,在函数传参时,我们需要传入指向数组的指针,而不是数组值。但,若是希望拿到该数组的副本,则可以选择使用传入值。
同时,指向数组的指针还必须指明数组的长度,这其实十分不方便。但切片可以很好地解决这个问题。
// 10的6次方数组
var array [1e6]int
foo(&array)
// 函数接受一个指向包含100万个整型值数组的指针
func foo(array *[1e6]int) {
...
}
3. 切片 引用-数据类型
切片是go自带的数据类型,围绕动态数组的概念来构建。
同时,切片是一个引用类型,所以切片的零值为nil
。
3.1. 切片的内部实现
切片其实是一个很小的结构体,对底层数组进行了抽象。“切片结构体”包含三个属性:
-
指向底层数组的指针。底层数组会一直存在,直到没有指向它的切片
-
切片的长度。动态数组的长度
-
切片的容量。容量相当于动态数组的长度上限
3.2. 切片的创建和初始化
-
格式为:
name []T
。注意[]
中无值,有值为数组 -
未初始化的切片为“空指针”,零值为
nil
-
用
make
关键字创建,还可以声明切片的长度和容量。推荐 -
通过数组或切片
[x:y]
来创建切片(包含x
位元素,排除y
位元素),可以使用[x:]
、[:y]
等,还可以通过[x:y:z]
规定切片的容量(z
)
// 切片的创建和初始化
// 注意切片和数组的区别
var slice1 []int // nil切片(空指针),指向底层数组的指针为空
if slice1 == nil {
fmt.Println("slice1 is nil")
}
slice2 := []int{0,1,2,3,5:6} // 创建并初始化,跟数组很像
slice3 := make([]int, 0) // 空切片(不是nil切片),长度和容量为0
slice4 := make([]string, 5) // 用make创建字符串切片,长度和容量都为5
slice5 := make([]int, 3, 5) // 长度为3,容量为5
fmt.Println(slice1, slice2, slice3, slice4, slice5)
arr := [5]int{0,1,2,3,4}
arrSlice1 := arr[1:3] // 用数组创建切片。此时切片指向该数组下标1位置,并且长度为2、容量为4(下标1到原数组结束)
arrSlice2 := arr[1:3:3] // 规定容量为3(容量不可超过原数组)。此时,arrSlice1和arrSlice2共享同一个底层数组
newSlice1 := slice2[1:3] // 用切片构建切片。两个切片共享一个底层数组
newSlice2 := newSlice1
fmt.Println(arr, arrSlice1, arrSlice2, newSlice1, newSlice2)
输出:
slice1 is nil
[] [0 1 2 3 0 6] [] [ ] [0 0 0]
[0 1 2 3 4] [1 2] [1 2] [1 2] [1 2]
3.3. 切片的使用
-
len()
函数获取切片长度,cap()
函数获取切片容量 -
func copy(dst, src []T) int
将src
切片的内容拷贝到dst
切片中,拷贝的长度为两个slice中长度较小的长度值 -
func append(s []T, x ...T) []T
返回一个新切片。当原切片容量不足时,append
函数会创建一个新的容量更大的底层数组,并将原切片的底层数组复制到新数组里,再追加新的值。append(dist, x, y)
追加多个值(x
,y
...)到dist
切片。append(dist, src...)
将整个src
切片追加到dist
切片尾。 -
切片的多维和遍历/迭代,与数组一样
// len() 和 cap()
fmt.Printf("slice5 is %v, len is %d, capacity is %d\n", slice5, len(slice5), cap(slice5))
fmt.Printf("arrSlice1 is %v, len is %d, capacity is %d\n", arrSlice1, len(arrSlice1), cap(arrSlice1))
// func copy(dst, src []T) int
copy(slice5, arrSlice1) // copy()函数会根据长度复制
fmt.Printf("slice5 is %v, len is %d, capacity is %d\n", slice5, len(slice5), cap(slice5))
// func append(s []T, x ...T) []T 返回一个新切片
// 当追加后,目标切片长度超过容量时,append函数会创建一个新的容量更大的底层数组,将原本数组复制到新数组中,再追加新的值
slice1 = append(slice1, 10)
fmt.Printf("slice1 is %v, len is %d, capacity is %d\n", slice1, len(slice1), cap(slice1))
slice1 = append(slice1, slice2...) // 用标识符 ... 将整个切片追加到另一个切片
fmt.Printf("slice1 is %v, len is %d, capacity is %d\n", slice1, len(slice1), cap(slice1))
输出:
slice5 is [0 0 0], len is 3, capacity is 5
arrSlice1 is [1 2], len is 2, capacity is 4
slice5 is [1 2 0], len is 3, capacity is 5
slice1 is [10], len is 1, capacity is 1
slice1 is [10 0 1 2 3 0 6], len is 7, capacity is 8
3.4. 切片的“陷阱”
切片赋值后,两个切片会共享同一个底层数组,一个切片修改值时会影响到另一个数组。切片共享底层数组示例图:
[图片上传失败...(image-b37feb-1601651171851)]
// 切片赋值后,会共用一个底层数组
sliceA := []string{"hello", "world", "!", "!", "!"}
fmt.Printf("sliceA is %v\n", sliceA)
sliceB := sliceA[:3]
sliceB[2] = "?" // 由于sliceB和sliceA共享一个底层数组,通过sliceB修改底层数组,会影响到sliceA
fmt.Printf("sliceB is %v\n", sliceB)
fmt.Printf("sliceA changs : %v\n", sliceA)
输出:
sliceA is [hello world ! ! !]
sliceB is [hello world ?]
sliceA changs : [hello world ? ! !]
4. 映射 引用-数据类型
映射又称map、键值对,基于特定的hash函数/散列函数。
映射也是引用类型,零值为nil
。
4.1. 映射的创建和初始化
-
格式:
name map[keyT]valueT
-
未初始化的声明会创建
nil
映射。nil 映射既没有键,也不能添加键 -
用
make()
函数进行创建,产生空映射而非nil
映射 -
用字面量初始化声明映射,采用换行的形式,需要在最后一个键值对后加
,
// 映射的创建和初始化
var dictNil map[string]int // 声明了一个nil映射,nil 映射既没有键,也不能添加键
if dictNil == nil {
fmt.Println("dictNil is nil")
}
// dictNil["red"] = 1 运行时会报错
dict1 := make(map[string]int) // 用make()函数创建的map,是空映射,而不是nil映射。映射/键值对中键的类型不能是切片、函数等引用数据类型
dict1["red"] = 1
dict2 := map[string]string {
"Red": "#da1337",
"Orange": "#e95a22", // 最后一行需要加 ,
}
fmt.Println(dict1, dict2)
输出:
dictNil is nil
map[red:1] map[Orange:#e95a22 Red:#da1337]
4.2. 映射的操作
-
获取值:
value=map[key]
。通过双赋值检测某个键是否存在:value, ok = map[key]
,若key
在map
中,ok
为true
;否则,ok
为false
;若key
不在映射中,那么value
是该映射元素类型的零值。 -
增加键值对:
map[key]=value
-
删除键值对,用
delete()
函数:delete(map, key)
fmt.Println(dict1["red"]) // 获取键值对
dict1["blue"] = 2 // 增加键值对
fmt.Println(dict1)
delete(dict1, "red") // 删除键值对
fmt.Println(dict1)
输出:
1
map[red:1 blue:2]
map[blue:2]
5. 结构体 值-数据类型
- 结构体类型使用关键字
struct
进行定义,用type
进行命名,如:type user struct{}
。
结构体定义:
type user struct {
name string
email string
age int
}
type admin struct {
person user // 嵌套另一个结构体
level int
}
结构体使用:
var bill user // 初始化后,结构体中所有成员都会被赋零值
// 短变量声明
lisa := user{
name: "Lisa",
email: "lisa@email.com",
age: 19, // 采用换行形式时最后一个也需要加 ,
}
tom := user{name:"Tom"} // 单独声明某一个成员
fmt.Println(bill, lisa, tom)
ad := admin{lisa, 10} // 直接按照结构体成员顺序,传入对应的值
adP := &ad // 获取指向结构体的指针
fmt.Println(ad, adP)
fmt.Println(ad.person.name) // 结构体值访问内部成员
fmt.Println(adP.person.name) // 结构体指针访问内部成员
输出:
{ 0} {Lisa lisa@email.com 19} {Tom 0}
{{Lisa lisa@email.com 19} 10} &{{Lisa lisa@email.com 19} 10}
Lisa
Lisa
6. new 和 make
在go语言中有两个用于内存分配的函数:new
和make
。
6.1. new
函数原型:
func new(Type) *Type
说明:
主要给值类型数据分配空间,并返回指向该数据空间的指针。这与c
语言中malloc
类似。但是,new()
函数不能指定个数和大小,只能传入指定的数据类型(包括用户自定义的数据类型)。
示例:
// var i *int
// *i=10 错误,野指针
var i *int
i = new(int)
arrP := new([5]int) // 分配长度为5的数组空间,并返回数组指针
6.2. make
函数原型:
func make(t Type, size ...IntegerType) Type
说明:
make也是用于内存分配的,但是和new不同,它只用于chan
、map
以及slice
的内存创建,而且它返回的类型值,而不是他们的指针。同时,make()
函数还能对这三个类型的相关属性进行初始化。
示例:
slice := make([]int, 3, 5) // 长度为3,容量为5,单位为int的slice
dict := make(map[string]int) // 键为string,值为int类型的map
buffer := make(chan string, 10)// 缓冲为10的字符串类型通道
7. 浅谈引用类型
在Go语言中,引用类型可以看作一个指针,它并不包含实际数据。比如,切片 slice
只是一个如下的类型:
type Slice struct {
point Point // 指向底层数据的指针
len int // 底层数据的长度
cap int // 底层数据的容量(最大长度)
}
当引用类型作为函数参数时,你可以通过引用类型修改所指向的数据(退出函数后依然有效)。但是,你不可以修改引用类型本身(退出函数后修改无效)。
以 map
为例:
func mapAdd(m map[string]interface{}) {
m["name"] = "dounine"
}
func mapAdd2(m map[string]interface{}) {
mp := map[string]interface{}{
"name": "dounine",
}
m = mp
}
func main() {
data3 := make(map[string]interface{})
mapAdd(data3)
fmt.Println("type:", reflect.TypeOf(data3), "; value:", reflect.ValueOf(data3))
data4 := make(map[string]interface{})
mapAdd2(data4)
fmt.Println("type:", reflect.TypeOf(data4), "; value:", reflect.ValueOf(data4))
}
输出:
type: map[string]interface {} ; value: map[name:dounine]
type: map[string]interface {} ; value: map[]
再以 slice
为例:
func sliceAdd(s []int) {
s = append(s, 6)
}
func sliceUpdate(s []int) {
s[0] = 1
}
func main() {
slice1 := []int{0, 1, 2, 3, 5}
sliceAdd(slice1)
fmt.Println(slice1)
sliceUpdate(slice1)
fmt.Println(slice1)
}
输出:
[0 1 2 3 5]
[1 1 2 3 5]
原因是:append
函数会修改 slice
类型本身的 len
属性,退出函数后失效;而修改 slice
类型指向的数组的值,退出函数后依然有效。
四、面向对象
在go
语言中是没有关键字class
的,也就是说,go
语言中没有类也没有继承。但,go
却是一个面向对象的语言,那它究竟如何实现面向对象呢?
首先,go
通过结构体的成员来定义类的属性,结构体名即类名;
其次,通过语法格式让函数与结构体关联,实现类方法;
然后,通过关键字interface
与结构体结合,实现接口和多态;
接着,通过结构体实名内嵌的形式,来实现对象内嵌另一个对象的has-a
模式;
最后,通过结构体匿名域内嵌的形式,来实现“继承”,即is-a
模式。
各功能的具体实现,下文一一讲解。
1. 类方法
1.1. 定义
格式:
// 用户类型
type user struct {
name string
email string
}
// notify 方法,以值为接收者
func (u user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n",
u.name,
u.email)
}
// changeEmail 方法,以指针为接收者
func (u *user) changeEmail(email string) {
u.email = email
}
说明:
-
一个类型的方法的声明,必须跟类型在同一个包内。
-
方法的声明与函数类似,不同的是,需要在
func
与方法名之间加上接收者参数,指明方法所从属的类型。接收者有两种:值接收者 和 指针接收者。
1.2. 调用类方法
调用类型的方法:<类型值/指针>.<方法>
,如:boss.notify()
。不管是值类型,还是指向类型的指针,都使用这种格式。
示例:
// 值类型
boss := user{
name: "aaaaa",
email: "123456",
age: 20,
}
boss.notify()
boss.changeEmail("2222") // go语言隐式转换,(&boss).changeEmail()
// 指针类型
bossP := &boss
bossP.notify() // 隐式转换,(*bossP).notify()
bossP.changeEmail("1111")
1.3. 指针接收者和值接收者
类型的值 使用 指针接收者声明的方法,和 类型的指针使用 值接收者声明的方法时,go语言都会进行隐式转换。所以,不管是以什么接收者声明的方法,值类型和指针类型都能调用。
示例:
boss := user{
name: "aaaaa",
email: "123456",
age: 20,
}
boss.notify()
boss.changeEmail("2222") // go语言隐式转换,(&boss).changeEmail()
fmt.Println(boss)
bossP := &boss
bossP.notify() // 隐式转换,(*bossP).notify()
bossP.changeEmail("1111")
fmt.Println(bossP)
值接收者 和 指针接收者 的区别:
- 值接收者得到类型的副本,修改副本值不会对原本值起作用;
- 指针接收者得到指向类型值的指针,所以,在指针接收者的方法中修改类型数据,会影响到原本的值。
示例:
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
// 值接收者
func (v Vertex) Scale1(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
// 指针接收者
func (v *Vertex) Scale2(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
fmt.Println(v) // 输出 {3,4}
v.Scale1(10)
fmt.Println(v) // 输出 {3,4}
v.Scale2(10)
fmt.Println(v) // 输出 {30,40}
}
2. 接口 引用-数据类型
2.1. 声明
接口是一系列方法的集合。它的格式如下:
type <接口名> interface {
方法1名(方法参数) 方法返回值
...
}
同时,也可以组合(嵌入)其它接口形成新接口:
type <接口名> interface {
接口名1
接口名2
...
方法1名(方法参数) 方法返回值
...
}
嵌入进来的接口,相当于把它的方法都复制到新接口中。
2.2. 实现接口
如果想要某个类型实现某个接口,只需要将接口中所有方法实现为类方法即可。示例:
type I interface {
M()
N()
}
type T struct {
S string
}
// 以下两个方法意味着: type T implements the interface I
func (t T) M() {
fmt.Println(t.S)
}
func (t T) N() {
fmt.Println(t.S)
}
2.3. 使用接口
如果某个类型实现了某个接口类型的所有方法,那么就可以将该类型的值或指针赋给这个接口类型的值。
但要注意:
- 值类型 和 指针类型 能使用 值接收者 实现的方法;
- 但是,值类型 不能使用 指针接收者 实现的方法,指针类型 才能使用。
示例如下:
// 接口 I
type I interface {
M()
}
// 类型 T
type T struct {
S string
}
// 值接收者 实现的方法
func (t T) M() {
fmt.Println(t.S)
}
func main() {
// 值类型 使用 值接收者 实现的方法
var i I = T{"hello"}
i.M()
// 指针类型 使用 值接收者 实现的方法
var i I = &T{"hello"}
i.M() // 隐式转换:(*i).M()
}
// 接口 I
type I interface {
M()
}
// 类型 T
type T struct {
S string
}
// 指针接收者 实现的方法
func (t *T) M() {
fmt.Println(t.S)
}
func main() {
// 值类型 不能使用 指针接收者 实现的方法
// Error: cannot use T literal (type T) as type I in assignment:
// T does not implement I (M method has pointer receiver)
var i1 I = T{"hello"}
i1.M()
// 指针类型 才能使用 指针接收者 实现的方法
var i2 I = &T{"hello"}
i2.M()
}
因此,我们通常定义类型指针来操作类型。
2.4. nil接口
如果没有为接口赋值,而调用接口中的方法,那将会报错,因为接口值为nil
:
type I interface {
M()
}
func main() {
var i I
// panic: runtime error: invalid memory address or nil pointer dereference
i.M()
}
2.5. 空接口 - 泛型
空接口相当于C++中的泛型。格式如下:
interface{}
使用示例:
package main
import "fmt"
func main() {
var i interface{}
describe(i) // 输出:(<nil>, <nil>)
i = 42
describe(i) // 输出:(42, int)
i = "hello"
describe(i) // 输出:(hello, string)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i) // %v:变量的值,%T:变量的类型
}
2.6. 类型断言与空接口(泛型)
类型断言可以判断变量是否为该类型。格式如下:
t := i.(T)
若不是,将报错中断程序。如果不想中断,则使用如下格式:
t, ok := i.(T)
若不是,则ok
值为false
。
使用示例:
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // panic: interface conversion: interface {} is string, not float64
fmt.Println(f)
}
实现一个类型判断函数:
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
2.7. 自定义输出格式 - Stringers 接口
Stringers
接口定义如下:
type Stringer interface {
String() string
}
它允许用户自定义变量打印格式。示例如下:
type Person struct {
Name string
Age int
}
// type Person implements the interface Stringer
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age) // 返回一个字符串
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z) // 输出:Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
}
2.8. 自定义错误处理 - error 接口
error
接口定义如下:
type error interface {
Error() string
}
使用如下:
import (
"fmt"
"time"
)
type MyError struct {
When time.Time
What string
}
// type MyError implements the interface error
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
// 返回一个 error 接口类型变量
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err) // at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work
}
}
3. 嵌入类型 - 继承
3.1. 声明
嵌入类型将已有类型直接声明在新的结构里,新的类型被称为外部类型,被嵌入的类型被称为内部类型。如下:
type <类型名> struct {
内部类型1名
...
属性1名
...
}
3.2. 创建
注意:创建时,依然需要区分出内部类型。因为外部类型有可能会覆盖内部类型中的标识符。 示例如下:
type user struct {
name string
email string
}
type admin struct {
user // 嵌入 user 类型,相当于 admin 继承了 user 类型
level string
}
func main() {
// Error: cannot use promoted field user.name in struct literal of type admin
// Error: cannot use promoted field user.email in struct literal of type admin
// ad := admin{
// name: "john",
// email: "qq.com",
// level: "1",
// }
// 创建类型,需要区别出内部类型
ad := admin{
user: user{
name: "john",
email: "qq.com",
},
level: "1",
}
}
3.2. 继承属性和方法
内部类型中的标识符(属性和方法)都会提升到外部类型中,就像直接在外部类型中声明了一样。
延续3.1中的例子:
// 可以通过内部类型访问内部类型的属性
fmt.Println(ad.user.name) // 输出:john
// 也可以直接访问内部类型的属性
fmt.Println(ad.name) // 输出:john
3.3. 覆盖属性和方法
外部类型也可以通过声明与内部类型同名的标识符,来覆盖内部标识符的属性或方法。这样,内部类型中对应的标识符将不会被提升,但其值依然存在。
示例:
type user struct {
name string
email string
}
type admin struct {
user
name string // 覆盖 user 类型中的 name 属性
level string
}
func main() {
// cannot use promoted field user.name in struct literal of type admin
// cannot use promoted field user.email in struct literal of type admin
ad := admin{
user: user{
name: "john",
email: "qq.com",
},
name: "tom",
level: "1",
}
fmt.Println(ad.name) // 输出:tom
fmt.Println(ad.user.name) // 输出:john
}
4. 公开或未公开的标识符 - 私有与公有
要使用另一个包中的类型时,类型名首字母需要大写,调用格式为:<package>.<name>
(package
为包名,name
为类型名)。
若要调用公开类型中的属性和方法时,属性和方法名的首字母也必须是大写。
示例:
package main
import (
"fmt"
"study/my_study/obj" // 导入另一个包
)
func main() {
// Person 为 study/my_study/obj 包中的类型
boss := obj.Person{
Name: "aaaaa",
Email: "123456",
}
// 调用 Person 类型的公开方法
boss.Notify()
boss.ChangeEmail("2222")
fmt.Println(boss)
}