为什么学习Golang语言

Go语言为并发而生

Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。开启一个goroutine的消耗非常小(大约2KB的内存),你可以轻松创建数百万个goroutine。
goroutine的特点:

  • goroutine具有可增长的分段堆栈。这意味着它们只在需要时才会使用更多内存。
  • goroutine的启动时间比线程快。
  • goroutine原生支持利用channel安全地进行通信。
  • goroutine共享数据结构时无需使用互斥锁。

Go性能强悍

Go语言简单易学

  • 语法简洁
  • 代码风格统一
  • 开发效率高

发展前景

大公司都在用,跟着主流走没错。

环境搭建

下载地址

Go官网下载地址:https://golang.org/dl/
Go官方镜像站(推荐):https://golang.google.cn/dl/

安装Golang

windwos/mac安装

安装方式基本都是下一步下一步傻瓜式安装,不多介绍

centos安装

下载压缩包

wget  https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz

下载好的文件解压到/usr/local目录下:

mkdir -p /usr/local/go  # 创建目录
tar -C /usr/lcoal/go zxvf go1.13.4.linux-amd64.tar.gz 

如果提示没有权限,加上sudo以root用户的身份再运行。执行完就可以在/usr/local/下看到go目录
配置环境变量: Linux下有两个文件可以配置环境变量,其中/etc/profile是对所有用户生效的,添加如下两行代码,保存退出

export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin

使用source命令加载/etc/profile文件即可生效。 检查:

> source /etc/profile
> go version

配置GOPATH

GOPATH是一个环境变量,用来表明你写的go项目的存放路径(工作目录)。
GOPATH路径最好只设置一个,所有的项目代码都放到GOPATH的src目录下。
Go1.11版本之后,开启go mod模式之后就不再强制需要配置GOPATH了。
Linux和Mac平台就参照上面配置环境变量的方式将自己的工作目录添加到环境变量中即可
GOPATH在不同操作系统平台上的默认值

在Windows 平台中GOPATH默认值是%USERPROFILE%/go,例如:C:Users用户名go
在Unix 平台中GOPATH默认值是$HOME/go,例如:/home/用户名/go

Go项目结构

在进行Go语言开发的时候,我们的代码总是会保存在$GOPATH/src目录下。在工程经过go build、go install或go get等指令后,会将下载的第三方包源代码文件放在$GOPATH/src目录下, 产生的二进制可执行文件放在 $GOPATH/bin目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下。

go mod 包管理极力推荐

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。
具体参考:https://github.com/golang/go/...

GO111MODULE

go moduleGO111MODULEoffonautoauto
GO111MODULE=offGOPATHvendorGO111MODULE=onGOPATHvendorgo.modGO111MODULE=auto$GOPATH/srcgo.mod
GO111MODULE=ongo module
go.modgo.sum

Go1.11之后设置GOPROXY命令为:

export GOPROXY=https://proxy.golang.org

go mod命令


go mod download    下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit        编辑go.mod文件
go mod graph       打印模块依赖图
go mod init        初始化当前文件夹, 创建go.mod文件
go mod tidy        增加缺少的module,删除无用的module
go mod vendor      将依赖复制到vendor下
go mod verify      校验依赖
go mod why         解释为什么需要依赖

Golang变量和常量

变量和常量是编程中必不可少的部分,也是很好理解的一部分。

标识符

在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。 举几个例子:abc, _, _123, a123。

关键字

关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。
Go语言中有25个关键字:

break         //退出循环
default     //选择结构默认项(switch、select)
func         //定义函数
interface    //定义接口
select        //channel
case         //选择结构标签
chan         //定义channel
const         //常量
continue     //跳过本次循环
defer         //延迟执行内容(收尾工作)
go         //并发执行
map         //map类型
struct        //定义结构体
else         //选择结构
goto         //跳转语句
package     //包
switch        //选择结构
fallthrough     //??
if         //选择结构
range         //从slice、map等结构中取元素
type        //定义类型
for         //循环
import         //导入包
return         //返回
var        //定义变量

Go语言中还有37个保留字

break     default     func     interface    select
case      defer       go       map          struct
chan      else        goto     package      switch
const     fallthrough if       range        type
continue  for         import   return       var

变量

变量的来历

程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。

变量类型

变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。

变量声明

声明格式

var 变量名 变量类型

变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:

var name string
var age int
var sex bool
var
var (
    a string
    b int
    c bool
    d float32
)

变量的初始化

Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。
变量初始化的标准格式如下:

var 变量名 类型 = 表达式

举例

var name string = "Q1mi"
var age int = 18

或
var name, age = "Q1mi", 20

可以使用更简略的 := 方式声明并初始化变量

n := 10
匿名变量
file, _ := os.Open("test.log")
Lua
:=_

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

const pi = 3.1415
const e = 2.7182
或
const (
    pi = 3.1415
    e = 2.7182
)

iota

iota是go语言的常量计数器,只能在常量的表达式中使用。

iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

例如

const (
        n1 = iota //0
        n2        //1
        n3        //2
        n4        //3
    )
    
const (
        a, b = iota + 1, iota + 2 //1,2
        c, d                      //2,3
        e, f                      //3,4
)

定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)

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

基本数据类型

Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异。

整型

整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

特殊整型

intuintintuint
len()intintuint

数字字面量语法(Number literals syntax)

Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:
v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。 v := 0o377,代表八进制的 377,相当于十进制的 255。 v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。 而且还允许我们用 _ 来分隔数字,比如说:

v := 123_456  // 123456。

浮点型

Go语言支持两种浮点型数:float32和float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。 float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。

打印浮点数时,可以使用fmt包配合动词%f,代码如下:

fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)

复数

complex64和complex128

var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

布尔值

booltrue(真)false(假)
false

字符串

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:

s1 := "hello"
s2 := "你好"

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

多行字符串

反引号
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

byte和rune类型

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:

var a := '中'
var b := 'x'

Go 语言的字符有以下两种:

uint8ASCII码runeUTF-8字符

类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
比如:计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。

func sqrtDemo() {
    var a, b = 3, 4
    var c int
    // math.Sqrt()接收的参数是float64类型,需要强制转换
    c = int(math.Sqrt(float64(a*a + b*b)))
    fmt.Println(c)
}

运算符

Go 语言内置的运算符有:算术运算符,关系运算符,逻辑运算符,位运算符,赋值运算符.

算术运算符

加+ 减 - 乘 * 除 / 求余 % 自增 ++ 自减 –
代码如下:

package main

import (
    "fmt"
)

func main()  {
    var a int = 21
    var b int = 10
    var c int

    c = a + b
    fmt.Printf("第一行 - c 的值为 %d\n", c )
    c = a - b
    fmt.Printf("第二行 - c 的值为 %d\n", c )
    c = a * b
    fmt.Printf("第三行 - c 的值为 %d\n", c )
    c = a / b
    fmt.Printf("第四行 - c 的值为 %d\n", c )
    c = a % b
    fmt.Printf("第五行 - c 的值为 %d\n", c )
    a++
    fmt.Printf("第六行 - a 的值为 %d\n", a )
    a=21   // 为了方便测试,a 这里重新赋值为 21
    a--
    fmt.Printf("第七行 - a 的值为 %d\n", a )

}

结果输出:

第一行 - c 的值为 31
第二行 - c 的值为 11
第三行 - c 的值为 210
第四行 - c 的值为 2
第五行 - c 的值为 1
第六行 - a 的值为 22
第七行 - a 的值为 20

关系运算符

== != > < >= <=
代码如下:

package main

import (
    "fmt"
)

func main()  {
    var a int = 21
    var b int = 10

    if( a == b ) {
        fmt.Printf("第一行 - a 等于 b\n" )
    } else {
        fmt.Printf("第一行 - a 不等于 b\n" )
    }
    if ( a < b ) {
        fmt.Printf("第二行 - a 小于 b\n" )
    } else {
        fmt.Printf("第二行 - a 不小于 b\n" )
    }

    if ( a > b ) {
        fmt.Printf("第三行 - a 大于 b\n" )
    } else {
        fmt.Printf("第三行 - a 不大于 b\n" )
    }
    /* Lets change value of a and b */
    a = 5
    b = 20
    if ( a <= b ) {
        fmt.Printf("第四行 - a 小于等于 b\n" )
    }
    if ( b >= a ) {
        fmt.Printf("第五行 - b 大于等于 a\n" )
    }
}

结果输出:

第一行 - a 不等于 b
第二行 - a 不小于 b
第三行 - a 大于 b
第四行 - a 小于等于 b
第五行 - b 大于等于 a

逻辑运算符

&& || !
代码如下:

package main

import (
    "fmt"
)

func main()  {
    var a bool = true
    var b bool = false
    if ( a && b ) {
        fmt.Printf("第一行 - 条件为 true\n" )
    }
    if ( a || b ) {
        fmt.Printf("第二行 - 条件为 true\n" )
    }
    /* 修改 a 和 b 的值 */
    a = false
    b = true
    if ( a && b ) {
        fmt.Printf("第三行 - 条件为 true\n" )
    } else {
        fmt.Printf("第三行 - 条件为 false\n" )
    }
    if ( !(a && b) ) {
        fmt.Printf("第四行 - 条件为 true\n" )
    }
}

结果输出:

第二行 - 条件为 true
第三行 - 条件为 false
第四行 - 条件为 true

位运算符

代码如下:

package main

import (
    "fmt"
)

func main()  {
    var a uint = 60    /* 60 = 0011 1100 */
    var b uint = 13    /* 13 = 0000 1101 */
    var c uint = 0

    c = a & b       /* 12 = 0000 1100 */
    fmt.Printf("第一行 - c 的值为 %d\n", c )

    c = a | b       /* 61 = 0011 1101 */
    fmt.Printf("第二行 - c 的值为 %d\n", c )

    c = a ^ b       /* 49 = 0011 0001 */
    fmt.Printf("第三行 - c 的值为 %d\n", c )

    c = a << 2     /* 240 = 1111 0000 */
    fmt.Printf("第四行 - c 的值为 %d\n", c )

    c = a >> 2     /* 15 = 0000 1111 */
    fmt.Printf("第五行 - c 的值为 %d\n", c )

}

结果输出:

第一行 - c 的值为 12
第二行 - c 的值为 61
第三行 - c 的值为 49
第四行 - c 的值为 240
第五行 - c 的值为 15

赋值运算符

= += -= *= /= %= <<= >>= &= ^= |=
代码如下:

package main

import (
    "fmt"
)

func main()  {
    var a int = 21
    var c int

    c =  a
    fmt.Printf("第 1 行 - =  运算符实例,c 值为 = %d\n", c )

    c +=  a
    fmt.Printf("第 2 行 - += 运算符实例,c 值为 = %d\n", c )

    c -=  a
    fmt.Printf("第 3 行 - -= 运算符实例,c 值为 = %d\n", c )

    c *=  a
    fmt.Printf("第 4 行 - *= 运算符实例,c 值为 = %d\n", c )

    c /=  a
    fmt.Printf("第 5 行 - /= 运算符实例,c 值为 = %d\n", c )

    c  = 200;

    c <<=  2
    fmt.Printf("第 6行  - <<= 运算符实例,c 值为 = %d\n", c )

    c >>=  2
    fmt.Printf("第 7 行 - >>= 运算符实例,c 值为 = %d\n", c )

    c &=  2
    fmt.Printf("第 8 行 - &= 运算符实例,c 值为 = %d\n", c )

    c ^=  2
    fmt.Printf("第 9 行 - ^= 运算符实例,c 值为 = %d\n", c )

    c |=  2
    fmt.Printf("第 10 行 - |= 运算符实例,c 值为 = %d\n", c )
}

结果输出:

第 1 行 - =  运算符实例,c 值为 = 21
第 2 行 - += 运算符实例,c 值为 = 42
第 3 行 - -= 运算符实例,c 值为 = 21
第 4 行 - *= 运算符实例,c 值为 = 441
第 5 行 - /= 运算符实例,c 值为 = 21
第 6行  - <<= 运算符实例,c 值为 = 800
第 7 行 - >>= 运算符实例,c 值为 = 200
第 8 行 - &= 运算符实例,c 值为 = 0
第 9 行 - ^= 运算符实例,c 值为 = 2
第 10 行 - |= 运算符实例,c 值为 = 2

其他运算符

& *
代码如下:

package main

import "fmt"

func main()  {
    var a int = 4
    var b int32
    var c float32
    var ptr *int

    /* 运算符实例 */
    fmt.Printf("第 1 行 - a 变量类型为 = %T\n", a );
    fmt.Printf("第 2 行 - b 变量类型为 = %T\n", b );
    fmt.Printf("第 3 行 - c 变量类型为 = %T\n", c );

    /*  & 和 * 运算符实例 */
    ptr = &a    /* 'ptr' 包含了 'a' 变量的地址 */
    fmt.Printf("a 的值为  %d\n", a);
    fmt.Printf("*ptr 为 %d\n", *ptr);

}

结果输出:

第 1 行 - a 变量类型为 = int
第 2 行 - b 变量类型为 = int32
第 3 行 - c 变量类型为 = float32
a 的值为  4
*ptr 为 4

流程控制

流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”。

Go语言中最常用的流程控制有if和for,而switch和goto主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。

if else(分支结构)

if
if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else{
    分支3
}

案例:

func Score(score int)  {
    if score >= 90 {
        fmt.Println("A")
    } else if score > 75 {
        fmt.Println("B")
    } else {
        fmt.Println("C")
    }
}

//可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断
func Score2(score int)  {
    if score := 65; score >= 90 {
        fmt.Println("A")
    } else if score > 75 {
        fmt.Println("B")
    } else {
        fmt.Println("C")
    }
}

for(循环结构)

for
for 初始语句;条件表达式;结束语句{
    循环体语句
}

条件表达式返回true时循环体不停地进行循环,直到条件表达式返回false时自动退出循环。
案例:

func forDemo() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}
//for循环的初始语句可以被忽略,但是初始语句后的分号必须要写
func forDemo2() {
    i := 0
    for ; i < 10; i++ {
        fmt.Println(i)
    }
}
//for循环的初始语句和结束语句都可以省略
func forDemo3() {
    i := 0
    for i < 10 {
        fmt.Println(i)
        i++
    }
}

无限循环

for {
    循环体语句
}
breakgotoreturnpanic

for range(键值循环)

for rangefor range
  1. 数组、切片、字符串返回索引和值。
  2. map返回键和值。
  3. 通道(channel)只返回通道内的值。

switch case

switch
func switchFinger(finger int) {
    switch finger {
    case 1:
        fmt.Println("大拇指")
    case 2:
        fmt.Println("食指")
    case 3:
        fmt.Println("中指")
    case 4:
        fmt.Println("无名指")
    case 5:
        fmt.Println("小拇指")
    default:
        fmt.Println("无效的输入!")
    }
}

//一个分支可以有多个值,多个case值中间使用英文逗号分隔
func jiou(n int) {
    switch n {
    case 1, 3, 5, 7, 9:
        fmt.Println("奇数")
    case 2, 4, 6, 8:
        fmt.Println("偶数")
    default:
        fmt.Println(n)
    }
}

//分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量
func getage(age int) {
    switch {
    case age < 25:
        fmt.Println("好好学习吧")
    case age > 25 && age < 35:
        fmt.Println("好好工作吧")
    case age > 60:
        fmt.Println("好好享受吧")
    default:
        fmt.Println("活着真好")
    }
}

//fallthrough语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
func stringprint(s string) {
    switch {
    case s == "a":
        fmt.Println("a")
        fallthrough
    case s == "b":
        fmt.Println("b")
    case s == "c":
        fmt.Println("c")
    default:
        fmt.Println("...")
    }
}

goto(跳转到指定标签)

goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:

package main

import "fmt"

func main()  {
    var breakAgain bool
    // 外循环
    for x := 0; x < 10; x++ {
        // 内循环
        for y := 0; y < 10; y++ {
            // 满足某个条件时, 退出循环
            if y == 2 {
                // 设置退出标记
                breakAgain = true
                // 退出本次循环
                break
            }
        }
        // 根据标记, 还需要退出一次循环
        if breakAgain {
            break
        }
    }
    fmt.Println("done")
}

代码说明如下:
第 10 行,构建外循环。
第 13 行,构建内循环。
第 16 行,当 y==2 时需要退出所有的 for 循环。
第 19 行,默认情况下循环只能一层一层退出,为此就需要设置一个状态变量 breakAgain,需要退出时,设置这个变量为 true。
第 22 行,使用 break 退出当前循环,执行后,代码调转到第 28 行。
第 28 行,退出一层循环后,根据 breakAgain 变量判断是否需要再次退出外层循环。
第 34 行,退出所有循环后,打印 done。

将上面的代码使用Go语言的 goto 语句进行优化:

package main

import "fmt"

func main()  {

    for x := 0; x < 10; x++ {
        for y := 0; y < 10; y++ {
            if y == 2 {
                // 跳转到标签
                goto breakHere
            }
        }
    }
    // 手动返回, 避免执行进入标签
    return
    // 标签
breakHere:
    fmt.Println("done")
}

break(跳出循环)

break语句可以结束for、switch和select的代码块。
break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的for、switch和 select的代码块上。

continue(继续下次循环)

continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。
在 continue语句后添加标签时,表示开始标签对应的循环。

数组(Array)

数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 基本语法:

// 定义一个长度为3元素类型为int的数组a
var a [3]int

数组的初始化

初始化数组时可以使用初始化列表来设置数组元素的值。

//数组会初始化为int类型的零值
var testArray [3]int  
//使用指定的初始值完成初始化
var numArray = [3]int{1, 2}       
//使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} 

按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:

var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}

数组的遍历

package main

import "fmt"

func main()  {

    var a = [...]string{"北京", "上海", "杭州","郑州"}
    // 方法1:for循环遍历
    for i := 0; i < len(a); i++ {
        fmt.Println(a[i])
    }

    // 方法2:for range遍历
    for index, value := range a {
        fmt.Println(index, value)
    }
}

多维数组

Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

package main
import "fmt"
func main()  {
    a := [3][2]string{
        {"北京", "上海"},
        {"广州", "深圳"},
        {"成都", "重庆"},
    }
    
    //二维数组的遍历
    for _, v1 := range a {
        for _, v2 := range v1 {
            fmt.Printf("%s\t", v2)
        }
        fmt.Println()
    }
    
    //支持的写法
    a := [...][2]string{
        {"北京", "上海"},
        {"广州", "深圳"},
        {"成都", "重庆"},
    }
    //不支持多维数组的内层使用...
    b := [3][...]string{
        {"北京", "上海"},
        {"广州", "深圳"},
        {"成都", "重庆"},
    }
}

切片(slice)

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

地址长度容量

切片的定义

声明切片类型的基本语法如下:

var name []T

其中,
name:表示变量名
T:表示切片中的元素类型

切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组定义切片。

// 基于数组定义切片
a := [5]int{55, 56, 57, 58, 59}
//基于数组a创建切片,包括元素a[1],a[2],a[3]
b := a[1:4] 

c := a[1:] //[56 57 58 59]
d := a[:4] //[55 56 57]
e := a[:]  //[55 56 57 58 59]

切片再切片

除了基于数组得到切片,我们还可以通过切片来得到切片。

package main

import "fmt"

func main()  {
    //切片再切片
    a := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}
    fmt.Printf("a:%v type:%T len:%d  cap:%d\n", a, a, len(a), cap(a))
    b := a[1:3]
    fmt.Printf("b:%v type:%T len:%d  cap:%d\n", b, b, len(b), cap(b))
    c := b[1:5]
    fmt.Printf("c:%v type:%T len:%d  cap:%d\n", c, c, len(c), cap(c))
}

输出:

a:[北京 上海 广州 深圳 成都 重庆] type:[6]string len:6  cap:6
b:[上海 广州] type:[]string len:2  cap:5
c:[广州 深圳 成都 重庆] type:[]string len:4  cap:4

注意:对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。

使用make()函数构造切片

动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:

make([]T, size, cap)

其中:

  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

举个例子:

a := make([]int, 2, 10)
fmt.Println(a)      //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

切片的基本操作

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

切片的赋值拷贝

拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容

s1 := make([]int, 3) //[0 0 0]
s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]

切片遍历

for range
s := []int{1, 3, 5}

for i := 0; i < len(s); i++ {
    fmt.Println(i, s[i])
}

for index, value := range s {
    fmt.Println(index, value)
}

append()方法为切片添加元素

Go语言的内建函数append()可以为切片动态添加元素。 每个切片会指向一个底层数组,这个数组能容纳一定数量的元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时。 举个例子:

func main() {
    //append()添加元素和切片扩容
    var numSlice []int
    for i := 0; i < 10; i++ {
        numSlice = append(numSlice, i)
        fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
    }
}
append()

append()函数还支持一次性追加多个元素。 例如:

var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]

使用copy()函数复制切片

copy()copy()
copy(destSlice, srcSlice []T)

其中:

  • srcSlice: 数据来源切片
  • destSlice: 目标切片

从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

func main() {
    // 从切片中删除元素
    a := []int{30, 31, 32, 33, 34, 35, 36, 37}
    // 要删除索引为2的元素
    a = append(a[:2], a[3:]...)
    fmt.Println(a) //[30 31 33 34 35 36 37]
}
indexa = append(a[:index], a[index+1:]...)

字典(Map)

key-value

map定义

map[KeyType]ValueType

其中:

  • KeyType:表示键的类型。
  • ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100

userInfo := map[string]string{
    "username": "张三",
    "password": "123456",
}

判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,格式如下:

value, ok := map[key]

map的遍历

for range
for k, v := range scoreMap {
    fmt.Println(k, v)
}

使用delete()函数删除键值对

delete()delete()
delete(map, key)

其中:

  • map:表示要删除键值对的map
  • key:表示要删除的键值对的键

按照指定顺序遍历map

package main

import (
    "fmt"
    "math/rand"
    "sort"
    "time"
)

func main()  {
    rand.Seed(time.Now().UnixNano()) //初始化随机数种子

    var scoreMap = make(map[string]int, 200)

    for i := 0; i < 100; i++ {
        key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
        value := rand.Intn(100)          //生成0~99的随机整数
        scoreMap[key] = value
    }
    //取出map中的所有key存入切片keys
    var keys = make([]string, 0, 200)
    for key := range scoreMap {
        keys = append(keys, key)
    }
    //对切片进行排序
    sort.Strings(keys)
    //按照排序后的key遍历map
    for _, key := range keys {
        fmt.Println(key, scoreMap[key])
    }
}

指针

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

ABBAB
&*

指针地址和指针类型

&*int*int64*string
ptr := &v    // v的类型为T

其中:

  • v:代表被取地址的变量,类型为T
  • ptr:用于接收地址的变量,ptr的类型就为T,称做T的指针类型。代表指针。

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

//指针取值
a := 10
// 取变量a的地址,将指针保存到b中
b := &a 
fmt.Printf("type of b:%T\n", b)
// 指针取值(根据指针去内存取值)
c := *b
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
&*&*

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

函数

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

函数定义

func
func 函数名(参数)(返回值){
    函数体
}
,(),

标准案例

package main

import "fmt"

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

func intSum2(x ...int) int {
    fmt.Println(x) //x是一个切片
    sum := 0
    for _, v := range x {
        sum = sum + v
    }
    return sum
}

func calc(x, y int) (int, int) {
    sum := x + y
    sub := x - y
    return sum, sub
}

func main(){
    ret := intSum(10, 20)
    fmt.Println(ret)
    ret1 := intSum2()
    ret2 := intSum2(10)
    ret3 := intSum2(10, 20)
    ret4 := intSum2(10, 20, 30)
    fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
}
intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

函数进阶

变量作用域

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x
for循环语句中定义的变量同样也属于局部变量,外部无法使用

package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testGlobalVar() {
    fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func testLocalVar() {
    //定义一个函数局部变量x,仅在该函数内生效
    var x int64 = 100
    fmt.Printf("x=%d\n", x)
}
func main() {
    testGlobalVar() //num=10
    testLocalVar() //x=100
    //fmt.Println(x) //undefined: x
    func testLocalVar3() {
    
    for i := 0; i < 10; i++ {
        fmt.Println(i) //变量i只在当前for语句块中生效
    }
    //fmt.Println(i) //此处无法使用变量i
}
}

函数类型与变量

定义函数类型
我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int, int) int

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

简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

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

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

add和sub都能赋值给calculation类型的变量。

var c calculation
c = add

高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

package main

import (
    "errors"
    "fmt"
)

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

func do(s string) (func(int, int) int, error) {
    switch s {
    case "+":
        return add, nil
    case "-":
        return sub, nil
    default:
        err := errors.New("无法识别的操作符")
        return nil, err
    }
}

func main() {
    ret2 := calc(10, 20, add)
    fmt.Println(ret2) //30
}

匿名函数和闭包

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

func(参数)(返回值){
    函数体
}

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
    // 将匿名函数保存到变量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20) // 通过变量调用匿名函数

    //自执行函数:匿名函数定义完加()直接执行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

闭包

闭包=函数+引用环境
func adder() func(int) int {
    var x int
    return func(y int) int {
        x += y
        return x
    }
}
func main() {
    var f = adder()
    fmt.Println(f(10)) //10
    fmt.Println(f(20)) //30
    fmt.Println(f(30)) //60

    f1 := adder()
    fmt.Println(f1(40)) //40
    fmt.Println(f1(50)) //90
}

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

defer语句

deferdeferdeferdeferdefer

举个例子

func main() {
    fmt.Println("start")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end")
}

panic/recover

panic/recoverpanicrecoverdefer
func funcA() {
    fmt.Println("func A")
}

func funcB() {
    panic("panic in B")
}

func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}
funcBpanicrecover
func funcA() {
    fmt.Println("func A")
}

func funcB() {
    defer func() {
        err := recover()
        //如果程序出出现了panic错误,可以通过recover恢复过来
        if err != nil {
            fmt.Println("recover in B")
        }
    }()
    panic("panic in B")
}

func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}

注意:

recover()deferdeferpanic

结构体

golang中并没有明确的面向对象的说法,实在要扯上的话,可以将struct比作其它语言中的class。

结构体的定义

typestruct
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}
  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。
Person
type person struct {
    name string
    city string
    age  int8
}

//或
type person struct {
    name, city string
    age        int8
}

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

var
var 结构体实例 结构体类型

基本实例化

    var p1 person
    p1.name = "zhiqiang"
    p1.city = "ZhengZhou"
    p1.age = 24
    fmt.Printf("p1=%v\n", p1)  //p1={zhiqiang ZhengZhou 24}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"zhiqiang", city:"ZhengZhou", age:24}
.p1.namep1.age

指针类型结构体

new
    p := new(person)
    p.name="zhiqiang"
    p.city="ZhengZhou"
    p.age=24
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

取结构体的地址实例化

&new
    p := &person{}
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"", age:0}
    p.name="zhiqiang"
    p.city="ZhengZhou"
    p.age=24
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

使用键值对初始化


p := person{name:"zhiqiang",city:"ZhengZhou",age:24}
fmt.Printf("p=%#v\n", p) //p=main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

p := &person{
    name: "zhiqiang",
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"", age:0}

或
p := &person{
    name: "zhiqiang",
    city: "ZhengZhou",
    age:  24,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"zhiqiang", city:"ZhengZhou", age:24}

匿名结构体

临时数据结构等场景下还可以使用匿名结构体

    var user struct{Name string; Age int}
    user.Name = "zhiqiang"
    user.Age = 24
    fmt.Printf("%#v\n", user)

构造函数

Go语言的结构体没有构造函数

personstruct
func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

调用构造函数

p := newPerson("zhiqiang", "ZhengZhou", 18)
fmt.Printf("%#v\n", p) //&main.person{name:"zhiqiang", city:"ZhengZhou", age:18}

方法和接收者

方法(Method)接收者(Receiver)thisself

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

其中,

selfthisPersonpConnectorc
package  main

import "fmt"

type person struct {
    name, city string
    age        int8
}
func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}
func (p person) Play()  {
    fmt.Printf("%s正在玩耍!\n", p.name)

}
func (p *person) SetAge(newage int8){
    p.age = newage
}
func (p *person) GetAge(){
    fmt.Printf("%s的年龄%d岁", p.name,p.age)

}
func main()  {
    p := newPerson("zhiqiang", "ZhengZhou", 18)
    p.GetAge()
}
thisselfPersonSetAge

值类型的接收者:当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

什么时候应该使用指针类型接收者

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

package  main

import "fmt"
//Person 结构体Person类型
type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "github",
        18,
    }
    fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
    fmt.Println(p1.string, p1.int) //北京 18
}

嵌套结构体

package main

import "fmt"

//Address 地址结构体
type Address struct {
    Province   string
    City       string
    CreateTime string
}

//Email 邮箱结构体
type Email struct {
    Account    string
    CreateTime string
}

//User 用户结构体
type User struct {
    Name   string
    Address
    Email
}


func main() {
    var u User
    u.Name = "zhiqiang"
    //嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
    u.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
    u.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
    fmt.Printf("u=%#v\n", u)
}

结构体的“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

package main

import "fmt"

//Animal 动物
type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
    Feet    int8
    *Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
    fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "乐乐",
        },
    }
    d1.wang() //乐乐会汪汪汪~
    d1.move() //乐乐会动!
}

结构体字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

结构体标签(Tag)

TagTag
`key1:"value1" key2:"value2"`
Tag

例如gorm模型定义:

type User struct {
    gorm.Model
    Birthday     time.Time
    Age          int
    Name         string  `gorm:"size:255"`       // string默认长度为255, 使用这种tag重设。
    Num          int     `gorm:"AUTO_INCREMENT"` // 自增

    CreditCard        CreditCard      // One-To-One (拥有一个 - CreditCard表的UserID作外键)
    Emails            []Email         // One-To-Many (拥有多个 - Email表的UserID作外键)

    BillingAddress    Address         // One-To-One (属于 - 本表的BillingAddressID作外键)
    BillingAddressID  sql.NullInt64

    ShippingAddress   Address         // One-To-One (属于 - 本表的ShippingAddressID作外键)
    ShippingAddressID int

    IgnoreMe          int `gorm:"-"`   // 忽略这个字段
    Languages         []Language `gorm:"many2many:user_languages;"` // Many-To-Many , 'user_languages'是连接表
}

接口( interface )

是对其他类型行为的概括和抽象。

interfacemethodduck-type programming

接口的定义

每个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    …
}

其中:

typeerWriterStringer

接口实现案例

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表

值接收者方式的dog

指针接收者实现接口的cat


package main

import "fmt"

// Sayer 接口
type Sayer interface {
    say()
}
type dog struct {}

func (d *dog) say() {
    fmt.Println("狗会动")
}
type cat struct {}
// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}
func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = &b       // 可以把dog实例指针直接赋值给x
    x.say()     // 汪汪汪
}

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

    // 定义一个空接口x
    var x interface{}

空接口的应用

使用空接口实现可以接收任意类型的函数参数。

使用空接口实现可以保存任意值的字典。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

// 空接口作为map值
var studentInfo = make(map[string]interface{})

接口值

一个具体类型具体类型的值动态类型动态值
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:

interface{}x
xTtruefalse
    var x interface{}
    x = "Hello Golang"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

反射

类型太多,然而类型断言又猜不全,使用反射能够直接拿到接口的动态类型和动态值

反射的应用

web框架,配置文件解析,orm框架

双刃剑

反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。

  1. 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
  2. 大量使用反射的代码通常难以理解。
  3. 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

reflect包

一个具体类型具体类型的值reflect.Typereflect.Valuereflect.TypeOfreflect.ValueOf

案例说明

package main

import (
    "fmt"
    "reflect"
)

func reflectType(x interface{}) {
    t := reflect.TypeOf(x)
    fmt.Printf("name:%v kid:%v\n", t.Name(),t.Kind())
}
func reflectValue(x interface{}) {
    v := reflect.ValueOf(x)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
        fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
    case reflect.Float32:
        // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
        fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
    case reflect.Float64:
        // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
        fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
    }
}

func reflectSetValue1(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(200) //修改的是副本,reflect包会引发panic
    }
}

func reflectSetValue2(x interface{}) {
    v := reflect.ValueOf(x)
    // 反射中使用 Elem()方法获取指针对应的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(200)
    }
}

func main() {
    var a float32 = 3.14
    reflectType(a) // name:float32 kid:float32
    reflectValue(a) //type is float32, value is 3.140000

    var b int64 = 100
    reflectType(b) // name:int64 kid:int64

    c := reflect.ValueOf(b)
    fmt.Printf("type c :%T\n", c) // type c :reflect.Value

    type book struct{ title string }
    var e =book{title:"Golang"}
    reflectType(e) //name:book kid:struct


    var f int64 = 100
    // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
    reflectSetValue2(&f)
    fmt.Println(f) //200

    // *int类型空指针
    var g *int
    fmt.Println("var a *int IsNil:", reflect.ValueOf(g).IsNil()) //var a *int IsNil: true

    // nil值
    fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) //nil IsValid: false

    // 实例化一个匿名结构体
    // 尝试从结构体中查找"abc"字段
    fmt.Println("不存在的结构体成员:", reflect.ValueOf(struct {}{}).FieldByName("abc").IsValid()) //不存在的结构体成员: false

    // 尝试从结构体中查找"abc"方法
    fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) //不存在的结构体方法: false

    // 尝试从map中查找一个不存在的键
    fmt.Println("map中不存在的键:", reflect.ValueOf(map[string]int64{}).MapIndex(reflect.ValueOf("hehe")).IsValid()) //map中不存在的键: false

}

.Name()空
reflect.ValueOf()reflect.Valuereflect.Value
reflect.Value
方法 说明
Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool 将值以 bool 类型返回
Bytes() []bytes 将值以字节数组 []bytes 类型返回
String() string 将值以字符串类型返回
IsNil()
IsValid()

结构体反射

reflect.TypeOf()reflect.TypeNumField()Field()
reflect.Type
方法 说明
Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。
NumField() int 返回结构体成员字段数量。
FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法

结构体反射示例

package main

import (
    "fmt"
    "reflect"
)

type student struct {
    Name  string `json:"name"`
    Score int    `json:"score"`
}
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
    msg := "好好学习,天天向上。"
    fmt.Println(msg)
    return msg
}

func (s student) Sleep() string {
    msg := "好好睡觉,快快长大。"
    fmt.Println(msg)
    return msg
}
func main() {
    stu := student{
        Name:  "张三",
        Score: 90,
    }

    t := reflect.TypeOf(stu)

    v := reflect.ValueOf(stu)



    fmt.Println(t.Name(), t.Kind()) // student struct
    // 通过for循环遍历结构体的所有字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
    }

    // 通过字段名获取指定结构体字段信息
    if scoreField, ok := t.FieldByName("Score"); ok {
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
    }


    fmt.Println(t.NumMethod())
    for i := 0; i < v.NumMethod(); i++ {
        methodType := v.Method(i).Type()
        fmt.Printf("method name:%s\n", t.Method(i).Name)
        fmt.Printf("method:%s\n", methodType)
        // 通过反射调用方法传递的参数必须是 []reflect.Value 类型
        var args = []reflect.Value{}
        v.Method(i).Call(args)
    }
}