Golang从入门到深入

zhiqiang · · 2749 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

为什么学习Golang语言

Go语言为并发而生

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

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

Go性能强悍

15511709770708.jpg
数据来源:https://benchmarksgame-team.p...

Go语言简单易学

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

发展前景

大公司都在用,跟着主流走没错。
image-20190203135218456.png

环境搭建

下载地址

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 下。
1550805044488.png

go mod 包管理极力推荐

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

GO111MODULE

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:offonauto,默认值是auto

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATHvendor文件夹中查找包。
  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATHvendor文件夹,只根据go.mod下载依赖。
  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

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

export GOPROXY=https://proxy.golang.org
阿里云配置如下:
export GOPROXY=https://mirrors.aliyun.com/go...

nexus社区提供配置如下:
export GOPROXY=https://gonexus.dev

goproxy.io的配置如下:
export GOPROXY=https://goproxy.io/

基于athens的公共服务配置如下:
export GOPROXY=https://athens.azurefd.net

官方提供的(jfrog,golang)
export GOPROXY=https://gocenter.io
export GOPROXY=https://proxy.golang.org

七牛云赞助支持的
export GOPROXY=https://goproxy.cn

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关键字会比较繁琐,go语言中还支持批量变量声明:

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

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,例如:

file, _ := os.Open("test.log")

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)
注意事项:

  1. 函数外的每个语句都必须以关键字开始(var、const、func等)
  2. :=不能使用在函数外。
  3. _多用于占位,表示忽略值。

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把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型。
image.png

特殊整型

image.png
注意:在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异。

注意事项:获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint

数字字面量语法(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位。

布尔值

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)false(假)两个值。
注意:

  1. 布尔类型变量的默认值为false
  2. Go 语言中不允许将整型强制转换为布尔型.
  3. 布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

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

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

字符串转义符

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

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

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

byte和rune类型

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

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

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

  1. uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
  2. rune类型,代表一个UTF-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(分支结构)

Go语言中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(循环结构)

Go 语言中的所有循环类型均可以使用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 {
    循环体语句
}

for循环可以通过breakgotoreturnpanic语句强制退出循环。

for range(键值循环)

Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for 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)
    }
}
  1. append()函数将元素追加到切片的最后并返回该切片。
  2. 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。

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

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

使用copy()函数复制切片

Go语言内建的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]
}

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

字典(Map)

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

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的遍历

Go语言中使用for range遍历map。

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

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

使用delete()内建函数从map中删除一组键值对,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个概念:指针地址、指针类型和指针取值。
任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话是要加载到内存(假设内存地址0x123456),我在程序中把这段话赋值给变量A,把内存地址赋值给变量B。这时候变量B就是一个指针变量。通过变量A和变量B都能找到我的座右铭。

Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*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语言中属于“一等公民”。

函数定义

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语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

举个例子

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

panic/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。panic可以在任何地方引发,但recover只有在defer调用的函数中有效。 首先来看一个例子:

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

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

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

程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行。

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()
}

注意:

  1. recover()必须搭配defer使用。
  2. defer一定要在可能引发panic的语句之前定义。

结构体

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 = "王志强"
    p1.city = "郑州"
    p1.age = 24
    fmt.Printf("p1=%v\n", p1)  //p1={王志强 郑州 24}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"王志强", city:"郑州", age:24}

我们通过.来访问结构体的字段(成员变量),例如p1.namep1.age等。

指针类型结构体

通过使用new关键字对结构体进行实例化,得到的是结构体的地址。

    p := new(person)
    p.name="王志强"
    p.city="郑州"
    p.age=24
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"王志强", city:"郑州", age:24}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

    p := &person{}
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"", city:"", age:0}
    p.name="王志强"
    p.city="郑州"
    p.age=24
    fmt.Printf("p=%#v\n", p) //p=&main.person{name:"王志强", city:"郑州", age:24}

使用键值对初始化


p := person{name:"王志强",city:"郑州",age:24}
fmt.Printf("p=%#v\n", p) //p=main.person{name:"王志强", city:"郑州", age:24}

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

或
p := &person{
    name: "王志强",
    city: "郑州",
    age:  24,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"王志强", city:"郑州", age:24}

匿名结构体

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

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

构造函数

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

实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

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

调用构造函数

p := newPerson("王志强", "郑州", 18)
fmt.Printf("%#v\n", p) //&main.person{name:"王志强", city:"郑州", age:18}

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

方法的定义格式如下:

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

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。
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("王志强", "郑州", 18)
    p.GetAge()
}

指针类型的接收者:指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。

值类型的接收者:当方法作用于值类型接收者时,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 = "王志强"
    //嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
    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)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如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 )

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

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则)

接口的定义

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

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

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

接口实现案例

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

值接收者方式的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)

其中:

  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

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

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

本文作者:王志强

阅读原文

如果文章有错误或者建议,请在评论区指出,非常感谢~


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:zhiqiang

查看原文:Golang从入门到深入

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2749 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传