golang 基础

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

/bin/   - > 存放编译后的二进制文件
/pkg/  - > 存放编译后的库文件
/src/   - > 存放源码文件 # 版本控制 只需要管理这个文件即可
       - 项目1
       - 项目2
       - 项目3

如果我们使用版本管理工具(Version Control System,VCS。常用如Git)来管理我们的项目代码时,我们只需要添加 $GOPATH/src目录的源代码即可。

bin 和 pkg 目录的内容无需版本控制。

Go语言中也是通过包来组织代码文件,我们可以引用别人的包也可以发布自己的包,但是为了防止不同包的项目名冲突,我们通常使用顶级域名来作为包名的前缀,这样就不担心项目名冲突的问题了。

因为不是每个个人开发者都拥有自己的顶级域名,所以目前流行的方式是使用个人的github用户名来区分不同的包

go get github.com/jmoiron/sqlx
那么,这个包会下载到我们本地GOPATH目录下的src/github.com/jmoiron/sqlx。

Hello World

现在我们来创建第一个Go项目——hello。在我们的GOPATH下的src目录中创建hello目录。

在该目录中创建一个main.go文件:

main.go

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt" // 导入内置 fmt 包

func main(){     // main函数,是程序执行的入口
     fmt.Println("Hello World!")   // 在终端打印 Hello World!
}

go build

go build 表示将源代码编译成可执行文件。

在hello目录下执行:

go build
或者在其他目录执行以下命令:

go build hello
go编译器会去 GOPATH的src目录下查找你要编译的hello项目

编译得到的可执行文件会保存在执行编译命令的当前目录下,如果是windows平台会在当前目录下找到hello.exe可执行文件。

可在终端直接执行该hello.exe文件:

d:\code\go\src\hello>hello.exe
Hello World
!

我们还可以使用-o参数来指定编译后可执行文件的名字。

go build -o heiheihei.exe

go install -> 会以 PATH去查找文件执行

go install表示安装的意思,它先编译源代码得到可执行文件,然后将可执行文件移动到GOPATH的bin目录下。因为我们的环境变量
中配置了GOPATH下的bin目录,所以我们就可以在任意地方直接执行可执行文件了。

跨平台编译

默认我们go build的可执行文件都是当前操作系统可执行的文件,如果我想在windows下编译一个linux下可执行文件,那需要怎么
做呢?

只需要指定目标操作系统的平台和处理器架构即可:

SET CGO_ENABLED=0 // 禁用CGO
SET GOOS=linux          // 目标平台是linux
SET GOARCH=amd64 // 目标处理器架构是amd64
// 然后再执行go build命令,得到的就是能够在Linux平台运行的可执行文件了。


Mac 下编译 Linux 和 Windows平台 64位 可执行程序:

Linux
CGO_ENABLED=0 
GOOS=linux 
GOARCH=amd64 
go build
windows 
CGO_ENABLED=0 
GOOS=windows 
GOARCH=amd64 
go build

Linux 下编译 Mac 和 Windows 平台64位可执行程序:

CGO_ENABLED=0 
GOOS=darwin 
GOARCH=amd64 
go build

CGO_ENABLED=0 
GOOS=windows 
GOARCH=amd64 
go build

Windows下编译Mac平台64位可执行程序:

SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build

Go语言基础之变量和常量 (业务逻辑 + 数据)

Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。
举几个例子:abc , _ , _123 , a123

关键字

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


Go语言中有25个关键字:

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

此外,Go语言中还有37个保留字。

Constants:   true false iota nil

Types:       int int8 int16 int32 int64 
             uint uint8 uint16 uint32 uint64 uintptr
             float32 float64 complex128 complex64
             bool byte rune string error

Functions:   make len cap new append copy close delete
             complex real imag
             panic recover

变量声明

Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。

标准声明

Go语言的变量声明格式为:

var  变量名  变量类型

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

var  name  string
var  age   int
var  isOk  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


类型推导

有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。

var name = "Q1mi"
var age  =  18

类型推导

package main
// 申明 包, main 表示 可独自执行的,每个独立的包里都会包含一个main包

import "fmt"
// 导入包,导入了fmt的包, 它包含的格式化输出的

func main(){ // 表示 执行函数
// 单行注释
/*
多行注释,块注释
*/
fmt.Printf("hello word!")
var (
     a = "safasf"
     b = 123
     c = true
     d = 0.123
    ) // 类型推导
fmt.Println(a,b,c,d)
}

短变量声明

在函数内部,可以使用更简略的  :=  方式声明并初始化变量。

package main
import "fmt"
// 全局变量m var m = 100 func main() { n := 10 m := 200 // 此处声明局部变量m fmt.Println(m, n) }

匿名变量

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

func foo() (int, string) {
     return 10, "Q1mi"
 }
func main() {
     x, _ := foo()
     _, y := foo()
     fmt.Println("x=", x)
     fmt.Println("y=", y)
}

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

注意事项:

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

常量

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

const pi = 3.1415
const e = 2.7182

声明了pi和e这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。

多个常量也可以一起声明:

iota

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

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

总结:
1. const 声明如果不写,默认就和上一行一样
2. iota 遇到 const 会清零
3. 每一行 iota 才会 +1 , 同一行内 不变


举个例子: iota 遇到 const 会自动+1

const (
    n1 = iota //0
    n2 //1
    n3 //2
    n4 //3
)

几个常见的iota示例:

使用_跳过某些值

const (
    n1 = iota //0
    n2 //1
    _
    n4 //3
)

iota声明中间插队

const (
    n1 = iota   //0
    n2 = 100   //100
    n3 = iota  //2
    n4 //3
)
const n5 = iota //0

定义数量级

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

多个iota定义在一行

const (
    a, b = iota + 1, iota + 2 //1,2
    c, d                      //2,3
    e, f                      //3,4
)
\     : 脱义,转义

`     : 反引号 原样输出
a
b
c
`

字符串的常用操作

方法 介绍

len(str)                求长度
+ 或 fmt.Sprintf        拼接字符串
strings.Split           分割
strings.Contains        判断是否包含
strings.HasPrefix, strings.HasSuffix    前缀/后缀判断
strings.Index(), strings.LastIndex()    子串出现的位置,index第一次出现 和lastindex最后一次出现的索引
strings.Join(a[]string, sep string)     join操作 ,连接字符串

byte和rune类型

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

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

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

uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。

Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理, 性能和扩展性都有照顾。

a := ''   // UTF-8 编码下 一个中文占 3个字节 3bit
b := 'G'    // ASCII码下占 一个字节 (8位 8bit)
            // rune 类型 代表一个 UTF-8类型

for range 循环 是按照 rune 类型去遍历的, 直接解决中英文混编

str := "hello你好"           // UTF8 编码 占3位
for _,v := range str{ 
    fmt.Printf("%d%c\n", v) // %c 是字符串替换,%d 是索引, k是索引 , v是值
}
go run main.go
结果: 0h 1e
2l 3l 4o 5你 // UTF8 编码 占3位 8好

类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换的基本语法如下:

T(表达式)

其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.

比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。

// 强制类型转换

s1 := "big"
// 将字符串强制转换成字节数组类型
byterArray := []byte(s1)
fmt.Println(byterArray)

byterArray[0] = 'p'
fmt.Println(byterArray)

// 将字节数组强制转换成字符串类型
s1 = string(byterArray)
fmt.Println(s1)
// hello 反转
// 方法1 
s1 := "hello"
byteArray := []byte(s1) // [h e l l o]
s2 := ""

for i:=len(byteArray)-1;i>=0;i--{
     // i 是 4 3 2 1 0
     // byteArray[i] o l l e h (字符)
    s2 += string(byteArray[i]) // 'h' 编码变成 字符串'h'
}
fmt.Println(s2)
// 方法2 
s1 := "hello"
abc:=[]byte(s1)
length := len(abc)
fmt.Println(length)
for i:=0; i<length/2; i++{
    fmt.Println(i)
    abc[i], abc[length-1-i] = abc[length-1-i] , abc[i]
    // 第一次循环 i=0 length=5 固定 -1
    // 第二次循环 i=1 length=5 固定 -1
    // 交叉赋值, 互换值
}
fmt.Println(string(abc)) // 最后把数组转回来字符串

关系运算符

运算符 描述

==    检查两个值是否相等,   如果相等返回 True 否则返回 False。
!=    检查两个值是否不相等, 如果不相等返回 True 否则返回 False。
>     检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>=    检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
<     检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<=    检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。

逻辑运算符

运算符 描述

&&    逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
||    逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
!     逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。


位运算符

位运算符对整数在内存中的二进制位进行操作。

&        参与运算的两数各对应的二进位相与。(两位均为1才为1)
|        参与运算的两数各对应的二进位相或。(两位有一个为1就为1)
^        参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1)
<<       左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>>       右移n位就是除以2的n次方。
“a>>b”   是把a的各二进位全部右移b位。


赋值运算符

运算符 描述

=     简单的赋值运算符,将一个表达式的值赋给一个左值
+=    相加后再赋值
-=    相减后再赋值
*=    相乘后再赋值
/=    相除后再赋值
%=    求余后再赋值
<<=   左移后赋值
>>=   右移后赋值
&=    按位与后赋值
|=    按位或后赋值
^=    按位异或后赋值
godoc -http=:9000   // 开启go的文档

Go语言基础之流程控制

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

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


if条件判断基本写法

Go语言中if条件判断的格式如下:

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

举个例子:

func ifDemo1() {
    score := 65
if 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++
}
}


这种写法类似于其他编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。

无限循环

for {
    循环体语句
}

for循环可以通过break、goto、return、panic语句强制退出循环。


for range(键值循环)

Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。
通过for range遍历的返回值有以下规律:

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

switch case

使用switch语句可方便地对大量的值进行条件判断。

func switchDemo1() {
    finger := 3
    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("无效的输入!")
}
}


Go语言规定每个switch只能有一个default分支。

一个分支可以有多个值,多个case值中间使用英文逗号分隔。

func testSwitch3() {
    switch n := 7; n {
    case 1, 3, 5, 7, 9:
        fmt.Println("奇数")
    case 2, 4, 6, 8:
        fmt.Println("偶数")
    default:
        fmt.Println(n)
}
}


分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:

func switchDemo4() {
    age := 30
    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 switchDemo5() {
    s := "a"
    switch {
        case s == "a":
              fmt.Println("a")
              fallthrough
        case s == "b":
              fmt.Println("b")
        case s == "c":
              fmt.Println("c")
        default:
            fmt.Println("...")
}
}

输出:

a
b

goto(跳转到指定标签)

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

例如双层嵌套的for循环要退出时:

func gotoDemo1() {
    var breakFlag bool
        for i := 0; i < 10; i++ {
            for j := 0; j < 10; j++ {
                if j == 2 {
                    // 设置退出标签
                    breakFlag = true
                    break
                 }
                 fmt.Printf("%v-%v\n", i, j)
             }
             // 外层for循环判断
             if breakFlag {
                 break
             }
        }  
}


使用goto语句能简化代码:

func gotoDemo2() {
    for i := 0; i < 10; i++ {
        for j := 0; j < 10; j++ {
            if j == 2 {
                // 设置退出标签
                goto breakTag
            }
            fmt.Printf("%v-%v\n", i, j)
        }
    }
    return
    // 标签
breakTag:
  fmt.Println("结束for循环")
}
200`1000 之前的质数
for i:= 200; i<1000; i++{
    flag:=true
    // 判断 i是否为质数,如果是就打印,如果不是就不打印
    for j:=2; j<i; j++{
        if i%j == 0{
            flag = false
            break
        }
    }
    // 整个第二次 for 循环结束了还没有break说明是质数
    if flag{
        fmt.Printf("%d是质数", i)
    }
}
9*9 乘法表
for i:=1; i<10 ;i++{
  for j:=1; j<=i; j++{
    fmt.Printf("%d*%d=%d\t",j,i,i*j)
  }
  fmt.Println()
}


Array(数组)

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

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

数组定义:

var 数组变量名 [元素数量]T

比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。

[5]int和[10]int是不同的类型。

var a [3]int
var b [4]int

a = b //不可以这样做,因为此时a和b是不同的类型
数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界, 会panic。

数组的初始化

数组的初始化也有很多方式。

方法一

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

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

方法二

按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,
例如:
func main() {
    var testArray [3]int
    var numArray = [...]int{1, 2}
    var cityArray = [...]string{"北京", "上海", "深圳"}
    fmt.Println(testArray)                          //[0 0 0]
    fmt.Println(numArray)                           //[1 2]
    fmt.Printf("type of numArray:%T\n", numArray)   //type of numArray:[2]int
    fmt.Println(cityArray)                          //[北京 上海 深圳]
    fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

方法三

我们还可以使用指定索引值的方式来初始化数组,例如:

func main() {
    a := [...]int{1: 1, 3: 5}
    fmt.Println(a)                  // [0 1 0 5]
    fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}


数组的遍历

遍历数组a有以下两种方法:

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语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

二维数组的定义

func main() {
    a := [3][2]string{
        {"北京", "上海"},
        {"广州", "深圳"},
        {"成都", "重庆"},
    }
    fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
    fmt.Println(a[2][1]) //支持索引取值:重庆
}

二维数组的遍历

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{
    {"北京", "上海"},
    {"广州", "深圳"},
    {"成都", "重庆"},
}


数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

func modifyArray(x [3]int) {
    x[0] = 100
}

func modifyArray2(x [3][2]int) {
    x[2][0] = 100
}
func main() {
    a := [3]int{10, 20, 30}
    modifyArray(a) //在modify中修改的是a的副本x
    fmt.Println(a) //[10 20 30]
    b := [3][2]int{
        {1, 1},
        {1, 1},
        {1, 1},
    }
    modifyArray2(b) //在modify中修改的是b的副本x
    fmt.Println(b)  //[[1 1] [1 1] [1 1]]
}

注意:

数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T表示指针数组,*[n]T表示数组指针 。

本文主要介绍Go语言中切片(slice)及它的基本使用。

引子

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:

func arraySum(x [3]int) int{
    sum := 0
    for _, v := range x{
        sum = sum + v
    }
    return sum
}


这个求和函数只能接受[3]int类型,其他的都不支持。 再比如,

a := [3]int{1, 2, 3}

数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了。

切片

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

切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

切片的定义

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

var name []T

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


举个例子:

func main() {
    // 声明切片类型
    var a []string              //声明一个字符串切片
    var b = []int{}             //声明一个整型切片并初始化
    var c = []bool{false, true} //声明一个布尔切片并初始化
    var d = []bool{false, true} //声明一个布尔切片并初始化
    fmt.Println(a)              //[]
    fmt.Println(b)              //[]
    fmt.Println(c)              //[false true]
    fmt.Println(a == nil)       //true
    fmt.Println(b == nil)       //false
    fmt.Println(c == nil)       //false
    // fmt.Println(c == d)   //切片是引用类型,不支持直接比较,只能和nil比较
}

切片的长度和容量

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

基于数组定义切片

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

func main() {
    // 基于数组定义切片
    a := [5]int{55, 56, 57, 58, 59}
    b := a[1:4]                     //基于数组a创建切片,包括元素a[1],a[2],a[3]
    fmt.Println(b)                  //[56 57 58]
    fmt.Printf("type of b:%T\n", b) //type of b:[]int
}


还支持如下方式:

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

切片再切片

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

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 : 切片的容量
func main() {
    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) 则返回该切片的容量。

切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息:

1 . 底层数组的指针

2 . 切片的长度(len)

3 . 切片的容量(cap)。

举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。

slice_01 切片s2 := a[3:6],相应示意图如下: slice_02

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和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来判断。

切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

func main() {
    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遍历。

func main() {
    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)
    }
}

输出:

[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000

从上面的结果可以看出:

append()函数将元素追加到切片的最后并返回该切片。
切片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) //[北京 上海 广州 深圳 成都 重庆]


切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
    newcap = cap
} else {
    if old.len < 1024 {
        newcap = doublecap
    } else {
        // Check 0 < newcap to detect overflow
        // and prevent an infinite loop.
        for 0 < newcap && newcap < cap {
            newcap += newcap / 4
        }
        // Set newcap to the requested cap when
        // the newcap calculation overflowed.
        if newcap <= 0 {
            newcap = cap
        }
    }
}


从上面的代码可以看出以下内容:

首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,

即 (newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)

如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。

使用copy()函数复制切片

首先我们来看一个问题:

func main() {
    a := []int{1, 2, 3, 4, 5}
    b := a
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(b) //[1 2 3 4 5]
    b[0] = 1000
    fmt.Println(a) //[1000 2 3 4 5]
    fmt.Println(b) //[1000 2 3 4 5]
}

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

copy(destSlice, srcSlice []T)

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


举个例子:

func main() {
    // copy()复制切片
    a := []int{1, 2, 3, 4, 5}
    c := make([]int, 5, 5)
    copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(c) //[1 2 3 4 5]
    c[0] = 1000
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(c) //[1000 2 3 4 5]
}


从切片中删除元素

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:]...)

练习题

1.请写出下面代码的输出结果。

func main() {
    var a = make([]string, 5, 10)
    for i := 0; i < 10; i++ {
        a = append(a, fmt.Sprintf("%v", i))
    }
    fmt.Println(a)
}
答案:[      0 1 2 3 4 5 6 7 8 9]  

# make 函数自动生成并初始化切片,也指定了前5个是空值,容量是10,append的放发有自动扩容的效果,
从第10位以后,当前的a 的内存地址就发生改变了。

2.请使用内置的sort包对数组 var a = [...]int{3, 7, 8, 9, 1} 进行排序。
导入的包 sort包

import "sort"   // 调用 sort.Ints() 方法进行排序,需要导入切片类型的数据才可以排序

var a = [...]int{3,7,8,9,1}    // 这是个数组 
sort.Ints(a[:])   // 快速切片,从到切到尾,把数组直接转成切片给sort.Ints()进行排序

fmt.Printl(a)

输出:
[1 3 7 8 9]

函数

函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了Go语言中函数的相关内容。

函数

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

函数定义

Go语言中定义函数使用func关键字,具体格式如下:

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

函数名 : 由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
参数   : 参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
返回值 : 返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
函数体 : 实现指定功能的代码块。

我们先来定义一个求两个数之和的函数:

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

函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:

func sayHello() {
    fmt.Println("Hello hc")
}

函数的调用

定义了函数之后,我们可以通过函数名()的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:

func main() {
    sayHello()
    ret := intSum(10, 20)
    fmt.Println(ret)
}


注意,调用有返回值的函数时,可以不接收其返回值。

参数

类型简写

函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:

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


上面的代码中,intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该
类型。

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。

注意:可变参数通常要作为函数的最后一个参数。

举个例子:

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


调用上面的函数:

ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4)  //0 10 30 60


固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:

func intSum3(x int, y ...int) int {
    fmt.Println(x, y)
    sum := x
    for _, v := range y {
        sum = sum + v
    }
    return sum
}


调用上述函数:

ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160


本质上,函数的可变参数是通过切片来实现的。

返回值

Go语言中通过return关键字向外输出返回值。

多返回值

Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

举个例子:

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


返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

例如:

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


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

输出结果:

start
end
3
2
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等

defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。

而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示: defer执行时机

defer经典案例

阅读下面的代码,写出最后的打印结果。

func f1() int {
    x := 5
    defer func() {
        x++
    }()
    return x
}
func f2() (x
int) { defer func() { x++ }() return 5 } func f3() (y int) { x := 5 defer func() { x++ }() return x } func f4() (x int) { defer func(x int) { x++ }(x) return 5 } func main() { fmt.Println(f1()) fmt.Println(f2()) fmt.Println(f3()) fmt.Println(f4()) }

defer面试题

func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}
问,上面代码的输出结果是?

函数进阶

变量作用域

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。

package main

import "fmt"

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

func testGlobalVar() {
    fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
    testGlobalVar() //num=10
}

局部变量

局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义
的变量x:

func testLocalVar() {
    //定义一个函数局部变量x,仅在该函数内生效
    var x int64 = 100
    fmt.Printf("x=%d\n", x)
}

func main() {
    testLocalVar()
    fmt.Println(x) // 此时无法使用变量x
}

如果局部变量和全局变量重名,优先访问局部变量。

package main

import "fmt"

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

func testNum() {
    num := 100
    fmt.Printf("num=%d\n", num) //函数中优先使用局部变量
}
func main() {
    testNum() //num=10
}

接下来我们来看一下语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。

func testLocalVar2(x, y int) {
    fmt.Println(x, y) //函数的参数也是只在本函数中生效
    if x > 0 {
        z := 100 //变量z只在if语句块生效
        fmt.Println(z)
    }
    //fmt.Println(z)//此处无法使用变量z
}
还有我们之前讲过的for循环语句中定义的变量,也是只在for语句块中生效:

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

函数类型变量

我们可以声明函数类型的变量并且为该变量赋值:

func main() {
    var c calculation               // 声明一个calculation类型的变量c
    c = add                         // 把add赋值给c
    fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
    fmt.Println(c(1, 2))            // 像调用add一样调用c

    f := add                        // 将函数add赋值给变量f1
    fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
    fmt.Println(f(10, 20))          // 像调用add一样调用f
}

高阶函数

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

函数作为参数

函数可以作为参数:

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 main() {
    ret2 := calc(10, 20, add)
    fmt.Println(ret2) //30
}


函数作为返回值

函数也可以作为返回值:

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


匿名函数和闭包

匿名函数

函数当然还可以作为返回值,但是在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也一直有效。 闭包进阶示
例1:

func adder2(x int) func(int) int {
    return func(y int) int {
        x += y
        return x
    }
}
func main() {
    var f = adder2(10)
    fmt.Println(f(10)) //20
    fmt.Println(f(20)) //40
    fmt.Println(f(30)) //70

    f1 := adder2(20)
    fmt.Println(f1(40)) //60
    fmt.Println(f1(50)) //110
}

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

func main() {
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Println(jpgFunc("test")) //test.jpg
    fmt.Println(txtFunc("test")) //test.txt
}

闭包进阶示例3:

func calc(base int) (func(int) int, func(int) int) {
    add := func(i int) int {
        base += i
        return base
    }

    sub := func(i int) int {
        base -= i
        return base
    }
    return add, sub
}

func main() {
    f1, f2 := calc(10)
    fmt.Println(f1(1), f2(2)) //11 9
    fmt.Println(f1(3), f2(4)) //12 8
    fmt.Println(f1(5), f2(6)) //13 7
}

闭包其实并不复杂,只要牢记闭包=函数+引用环境。


内置函数介绍

内置函数 介绍

close         主要用来关闭channel
len           用来求长度,比如string、array、slice、map、channel
new           用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make          用来分配内存,主要用来分配引用类型,比如chan、map、slice
append        用来追加元素到数组、slice中
panic和recover    用来做错误处理
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()
}
输出: func A panic: panic
in B goroutine 1 [running]: main.funcB(...) .../code/func/main.go:12 main.main() .../code/func/main.go:20 +0x98 // 程序运行期间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() } 注意: recover()必须搭配defer使用。 defer一定要在可能引发panic的语句之前定义。


/*
你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:
a. 名字中每包含1个'e'或'E'分1枚金币
b. 名字中每包含1个'i'或'I'分2枚金币
c. 名字中每包含1个'o'或'O'分3枚金币
d: 名字中每包含1个'u'或'U'分4枚金币
写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现 ‘dispatchCoin’ 函数
*/

package main
import "fmt"
var (
    coins = 50
    users = []string{
        "Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
    }
    distribution = make(map[string]int, len(users))
                   //   map [字符串] int类型,容量是users名的长度
                   //   map[key的类型是字符串]值的类型是int类型, 值values
)

func dispatchCoin() int{
    var number=0
    for _,name := range users    {
        var ret = 0
        if strings.ContainsAny(name, "e&E" ){
            num := strings.Count(name,"e")
            num2 := strings.Count(name,"E")
            ret += num+num2
        }
        if strings.ContainsAny(name, "i&I" ) {
            num := strings.Count(name,"i")
            num2 := strings.Count(name,"T")
            ret += (num*2)+(num2*2)
        }
        if strings.ContainsAny(name, "o&O" ) {
            num := strings.Count(name,"o")
            num2 := strings.Count(name,"O")
            ret +=(num*3)+(num2*3)
        }
        if strings.ContainsAny(name, "u&U" ) {
            num := strings.Count(name,"u")
            num2 := strings.Count(name,"U")
            ret +=(num*4)+(num2*4)
        }

        distribution[name]= ret
        number += distribution[name]
    }
    //fmt.Println(distribution)
    return  (coins-number)
}

func main() {
     //
     //fmt.Println(strings.ContainsAny(a,"t&T"))
     //fmt.Println(strings.Count(a,"T"))

    left := dispatchCoin()
    fmt.Println("剩下:", left)
}

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

要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

Go语言中的指针

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指
针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据
地址取值)。

指针地址和指针类型

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

& :取地址
* :根据地址取值


取变量指针的语法如下:

ptr := &v // v的类型为T

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

举个例子:

func main() {
    a := 10
    b := &a
    fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
    fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
    fmt.Println(&b)                    // 0xc00000e018
}

我们来看一下b := &a的图示: 取变量地址图示

指针取值

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

func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    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)
}
输出如下: type of b:
*int type of c:int value of c:10 总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

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

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

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100
}
 

new和make

我们先来看一个例子:

func main() {
    var a *int
    *a = 100
    fmt.Println(*a)

    var b map[string]int
    b["hc"] = 100
    fmt.Println(b)
}

执行上面的代码会引发panic,为什么呢?

在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。

而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间
要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存

new

new是一个内置的函数,它的函数签名如下:

func new(Type) *Type

Type : 表示类型,new函数只接受一个参数,这个参数是一个类型
*Type : 表示类型指针,new函数返回一个指向该类型内存地址的指针。
new : 函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。
func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}    

本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,
才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:

func main() {
    var a *int
    a = new(int)
    *a = 10
    fmt.Println(*a)
}


make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是
他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。

本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始
化操作之后,才能对其进行键值对赋值:

func main() {
    var b map[string]int
    b = make(map[string]int, 10)
    b["hc"] = 100
    fmt.Println(b)
}
new与make的区别

二者都是用来做内存分配的。
make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。


Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高
的扩展性和灵活性。

类型别名和自定义类型

自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

//将MyInt定义为int类型
type MyInt int


通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。

就像一个孩子小时候有小名、乳名,上学后用 学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

type TypeAlias = Type

我们之前见过的rune和byte就是类型别名,他们的定义如下:

type byte = uint8
type rune = int32


类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt
    
    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。

也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

结构体的定义

使用type和struct关键字来定义结构体,具体代码格式如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

类型名 : 标识自定义结构体的名称,在同一个包内不能重复。
字段名 : 表示结构体字段名。结构体中的字段名必须唯一。
字段类型 : 表示结构体字段的具体类型。


举个例子,我们定义一个Person(人)结构体,代码如下:

type person struct {
name string
city string
age int8
}
同样类型的字段也可以写在一行,

type person struct {
    name string
    city string
    age  int8
}


这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。

这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。

比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

结构体实例化

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

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型

基本实例化

举个例子:

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p1 person
    p1.name = "hc"
    p1.city = "北京"
    p1.age = 18
    fmt.Printf("p1=%v\n", p1)  //p1={hc 北京 18}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"hc", city:"北京", age:18}
}

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

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

package main
     
import (
    "fmt"
)
     
func main() {
    var user struct{Name string; Age int}
    user.Name = "hc"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}


创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

var p2 = new(person)
fmt.Printf("%T\n", p2)     //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

var p2 = new(person)
p2.name = "hc"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"hc", city:"上海", age:28}


取结构体的地址实例化

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

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}
p3.name = "七米"其实在底层是(*p3).name = "七米",这是Go语言帮我们实现的语法糖。

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值。

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

p5 := person{
    name: "hc",
    city: "北京",
    age:  18,
}

fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}


也可以对结构体指针进行键值对初始化,例如:

p6 := &person{
    name: "hc",
    city: "北京",
    age:  18,
}

fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

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

使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

p8 := &person{
    "hc",
    "北京",
    28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"hc", city:"北京", age:28}


使用这种格式初始化时,需要注意:

必须初始化结构体的所有字段。
初始值的填充顺序必须与字段在结构体中的声明顺序一致。
该方式不能和键值初始化方式混用。
结构体内存布局

结构体占用一块连续的内存。

type test struct {
    a int8
    b int8
    c int8
    d int8
}
n := test{
    1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
输出: n.a
0xc0000a0060 n.b 0xc0000a0061 n.c 0xc0000a0062 n.d 0xc0000a0063

【进阶知识点】关于Go语言中的内存对齐推荐阅读:在 Go 中恰到好处的内存对齐

构造函数

Go语言的结构体没有构造函数,我们可以自己实现。

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

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


调用构造函数

p9 := newPerson("张三", "hc", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"hc", age:90}


方法和接收者

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

方法的定义格式如下:

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

接收者变量 : 接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。
例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
接收者类型 : 接收者类型和参数类似,可以是指针类型和非指针类型。

方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

//Person 结构体
type Person struct {
    name string
    age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}

//Dream Person做梦的方法
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := NewPerson("hc", 25)
    p1.Dream()
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

指针类型的接收者

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

// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
    p.age = newAge
}
调用该方法:

func main() {
    p1 := NewPerson("hc", 25)
    fmt.Println(p1.age) // 25
    p1.SetAge(30)
    fmt.Println(p1.age) // 30
}

值类型的接收者

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

// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("hc", 25)
    p1.Dream()
    fmt.Println(p1.age) // 25
    p1.SetAge2(30) // (*p1).SetAge2(30)
    fmt.Println(p1.age) // 25
}

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

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

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
    fmt.Println("Hello, 我是一个int。")
}
func main() {
    var m1 MyInt
    m1.SayHello() //Hello, 我是一个int。
    m1 = 100
    fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
}

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

结构体的匿名字段

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

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

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

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

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

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

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

func main() {
    user1 := User{
        Name:   "hc",
        Gender: "",
        Address: Address{
            Province: "山东",
            City:     "威海",
        },
    }
    fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"hc", Gender:"男", Address:main.Address

{Province:"山东", City:"威海"}}
}

嵌套匿名结构体

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

//User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address //匿名结构体
}

func main() {
    var user2 User
    user2.Name = "hc"
    user2.Gender = ""
    user2.Address.Province = "山东"    //通过匿名结构体.字段名访问
    user2.City = "威海"                //直接访问匿名结构体的字段名
    fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"hc", Gender:"男", Address:main.Address

{Province:"山东", City:"威海"}}
}

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

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

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

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

func main() {
    var user3 User
    user3.Name = "hc"
    user3.Gender = ""
    // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
    user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
    user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
}

结构体的“继承”

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

//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() //乐乐会动!
}

结构体字段的可见性

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

结构体与JSON序列化

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值
对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间
使用英文,分隔。

//Student 学生
type Student struct {
    ID     int
    Gender string
    Name   string
}

//Class 班级
type Class struct {
    Title    string
    Students []*Student
}

func main() {
    c := &Class{
        Title:    "101",
        Students: make([]*Student, 0, 200),
    }
    for i := 0; i < 10; i++ {
        stu := &Student{
            Name:   fmt.Sprintf("stu%02d", i),
            Gender: "",
            ID:     i,
        }
        c.Students = append(c.Students, stu)
    }
    //JSON序列化:结构体-->JSON格式的字符串
    data, err := json.Marshal(c)
    if err != nil {
        fmt.Println("json marshal failed")
        return
    }
    fmt.Printf("json:%s\n", data)
    //JSON反序列化:JSON格式的字符串-->结构体
    str := `{"Title":"101","Students":[{"ID":0,"Gender":"","Name":"stu00"},
            {"ID":1,"Gender":"","Name":"stu01"},{"ID":2,"Gender":"","Name":"stu02"},
            {"ID":3,"Gender":"","Name":"stu03"},{"ID":4,"Gender":"","Name":"stu04"},
            {"ID":5,"Gender":"","Name":"stu05"},{"ID":6,"Gender":"","Name":"stu06"},
            {"ID":7,"Gender":"","Name":"stu07"},{"ID":8,"Gender":"","Name":"stu08"},
            {"ID":9,"Gender":"","Name":"stu09"}]}`
    c1 := &Class{}
    err = json.Unmarshal([]byte(str), c1)
    if err != nil {
        fmt.Println("json unmarshal failed!")
        return
    }
    fmt.Printf("%#v\n", c1)
}

结构体标签(Tag)

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

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

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

//Student 学生
type Student struct {
    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
    Gender string //json序列化是默认使用字段名作为key
    name   string //私有不能被json包访问
}

func main() {
    s1 := Student{
        ID:     1,
        Gender: "",
        name:   "hc",
    }
    data, err := json.Marshal(s1)
    if err != nil {
        fmt.Println("json marshal failed!")
        return
    }
    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}

未完待续。。。

猜你喜欢

转载自www.cnblogs.com/huidou/p/11400334.html