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.Printffmt.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中iffor语句不需要加();语句大括号{不需要换行。

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关键字,可以遍历顺序结构,返回keyvalue
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进行声明
  • 返回类型(和值),放在参数项的后面
  • 允许先声明返回值
  • 多值返回
  • deferdefer语句会将指定函数推迟到外层函数返回之后再执行。并且,被推迟的函数将被压入一个栈中。
//==========函数============
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) intsrc切片的内容拷贝到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],若keymap中,oktrue;否则,okfalse;若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语言中有两个用于内存分配的函数:newmake

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不同,它只用于chanmap以及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)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容