go基础第一遍学习(李文周博客)

go基础第一遍学习记录,以下内容来自李文周博客

文章目录

  • my重点记录
      • 1.println和prinf的区别
          • 一、通常情况:
          • 二、整数:
          • 三、浮点数:
          • 四、字符串:
          • 五、布尔类型:
    • 2.[golang fmt格式“占位符”](https://www.cnblogs.com/qing123/articles/4353353.html)
  • 一.开发环境准备
      • 1.go语言的强悍
      • 2.安装Go语言及搭建Go语言开发环境
        • 2.1 GOROOT和GOPATH
        • 2.2 GOPROXY 非常重要
      • 3.第一个go程序
          • go run
          • go install
      • 4.依赖管理
          • 4.1 为什么需要依赖管理
          • 4.2 godep
          • 4.3 go module
            • 4.3.1 GO111MODULE
          • 4.4 go mod命令
          • 4.5 go.mod
          • 4.6 依赖的版本
          • 4.7 replace()
          • 4.8 go get(下载依赖包)
          • 4.9 整理依赖
          • 4.10 go mod edit
        • 在项目中使用go module
          • 既有项目
          • 新项目
      • 5.如何使用go module导入本地包
          • 5.1 同一个项目下
          • 5.2 不同项目下
  • 二、go基础
    • 一、Go语言基础之变量和常量
      • 1.标识符与关键字
          • 1.1 标识符
          • 1.2 关键字
      • 2.变量
          • 2.1 变量的来历
          • 2.2 变量类型
          • 2.3 声明变量
          • 2.4 变量的初始化
            • 2.4.1 变量初始化标准格式
            • 2.4.2 类型推导
            • 2.4.3 短变量声明
            • 2.4.4 匿名变量
          • 2.5 变量默认值
      • 3.常量
          • 3.1 iota
            • 基础例子
            • 使用`_`跳过某些值
            • `iota`声明中间插队
            • 多个`iota`定义在一行
    • 二、Go语言基础之基本数据类型
      • 1.数据类型
        • 1.1 整型
        • 1.2 浮点型
        • 1.3 复数
        • 1.4 布尔值
        • 1.5 字符串
          • 1.5.1 字符串转义符
          • 1.5.2 多行字符串
          • 1.5.3 字符串常用操作
        • 1.6 byte和rune类型
      • 2.类型转换
    • 三、go语言之运算符
    • 四、go语言基础之流程控制
      • 1. if else(分支结构)
          • 1.1 if条件判断
          • 1.2 if条件的特殊写法
      • 2. for(循环结构)
        • 2.1 for基础
          • 无限循环
        • 2.2 for range(键值循环)
      • 3.switch case语句
      • 4.goto(跳转到指定标签)
      • 5.break跳出循环
      • 6.continue继续下次循环
    • 五、Go语言基础之数组
      • 1.数组定义
      • 2.数组的初始化
          • 2.1 方法一
          • 2.2 方法二
          • 2.3 方法三
      • 3.数组的遍历
      • 4.多维数组
      • 5.数组是值类型
    • 六、Go语言基础之切片
      • 1.切片的定义
          • 1.1 切片的长度和容量
          • 1.2 切片表达式
          • 1.3 使用make()函数构造切片
          • 1.4 切片的本质
          • 1.5 判断切片是否为空
      • 2.切片不能直接比较
      • 3.切片的赋值拷贝
      • 4.切片的遍历
      • 5.append()方法为切片添加元素
      • 6.切片的扩容策略
      • 7.使用copy()函数复制切片
      • 8.从切片中删除元素
    • 七、Go语言基础之map
      • 1.map定义
      • 2.map基本使用
      • 3.判断某个键是否存在
      • 4.map遍历
      • 5.使用delete()函数删除键值对
      • 6.按照指定顺序遍历map
      • 7.元素为map类型的切片
      • 7.值为切片类型的map
    • 八、go语言基础之函数
      • 1.函数定义
    • 八、go语言基础之函数
      • 1.函数
        • 1.函数定义
        • 2.函数的调用
        • 3.参数
          • 3.1 类型简写
          • 3.2 可变参数
        • 4.返回值
          • 4.1 多返回值
          • 4.2 返回值命名
          • 4.3 返回值补充
      • 2.函数进阶
        • 1.变量作用域
          • 1.1 全局变量
          • 1.2 局部变量
        • 2.函数类型与变量
          • 2.1 定义函数类型
          • 2.2 函数类型变量
        • 3.高阶函数
          • 3.1 函数作为参数
          • 3.2 函数作为返回值
        • 4.匿名函数和闭包
          • 4.1 匿名函数
          • 4.2 闭包
        • 5.defer语句
          • 5.1 defer执行时机
          • 5.2 defer经典案例
      • 3.内置函数
        • 3.1 panic/recover
    • 九、go语言基础之指针
      • 1.指针地址和指针类型
      • 2.指针取值
      • 3.new和make
        • 3.1 new
        • 3.2 make
        • 3.3 new和make的区别
    • 十、go语言基础之结构体
      • 1.类型别名和自定义类型
        • 1.1 自定义类型
        • 1.2 类型别名
        • 1.3 类型定义和类型别名的区别
      • 2.结构体
        • 2.1 构体的定义
        • 2.2 结构体实例化
          • 2.2.1 基本实例化
          • 2.2.2 匿名结构体
          • 2.2.3 创建指针类型结构体
          • 2.2.4 取结构体的地址实例化
      • 3.结构体初始化
        • 3.1 使用键值对初始化
        • 3.2 使用值的列表初始化
      • 4.结构体内存布局
        • 4.1 空结构体
      • 5.面试题
      • 6.构造函数
      • 7.方法和接收者
        • 7.1 指针类型的接收者
        • 7.2 值类型的接收者
        • 7.3 什么时候应该使用指针类型接收者
      • 8.任意类型添加方法
      • 9.结构体的匿名字段
      • 10.嵌套结构体
        • 10.1 嵌套匿名字段
        • 10.2 嵌套结构体的字段名冲突
      • 11.结构体的“继承”
      • 12.结构体字段的可见性
      • 13.结构体与JSON序列化
      • 14.结构体标签(Tag)
      • 15.结构体和方法补充知识点
    • 十一、go语言基础之包
      • 1.包
        • 1.1 包介绍
        • 1.2 定义包
        • 1.3 标识可见性
        • 1.4 包引入
        • 1.5 init初始化函数
      • 2.go module
        • 2.1 go module介绍
        • 2.2 使用go module引入包
    • 十二、Go语言基础之接口
      • 1.1 为什么要使用接口
      • 1.2 使用值接收和使用指针接收的区别
      • 1.3 类型和接口的关系
      • 1.4 空接口
      • 1.5 断言
    • 十三、go基础之反射
    • casbin

my重点记录

1.println和prinf的区别

https://blog.csdn.net/Fe_cow/article/details/103804689

首先 Println 跟 Printf 都是fmt包中的公共方法

Println:打印字符串变量

Printf:打印需要格式化的字符串,可以输出字符串类型的变量;不可以输出整型变量和整型;

简单理解,当需要格式化输出信息时,一般选择Printf,其余使用Println

一、通常情况:

%v:默认方式打印变量的值;

%T:打印变量的类型;

二、整数:

%+d:带符号的整型;

%q:打印单引号;

%o: 不带零的八进制;

%#o: 带零的八进制;

%x: 小写的十六进制;

%X: 大写的十六进制;

%#x: 带0x的十六进制;

%U: 打印Unicode字符;

%#U: 打印带字符的Unicode;

%b: 打印整型的二进制;

三、浮点数:

%f:6位小数点;

%.6f: 6位小数点;

%e:6位小数点(科学计数法);

%g:最少的数字来表示;1

%.3g:最多3位数字表示;

i := 12.123456789

fmt.Printf(“i: %.3g”,i)

// 输出结果

12.1

%.3f:最多3位小数表示;

i := 12.123456789

fmt.Printf(“i: %.3g”,i)

// 输出结果

12.123

四、字符串:

%s:正常输出字符串;

%q: 字符串带双引号,字符串中的引号带转义符;

%#q: 字符串带反引号,如果字符串内有反引号,就用双引号代替;

%x: 将字符串转换为小写的16进制格式;

%X: 将字符串转换为大写的16进制格式;

% x: 带空格的16进制格式;

五、布尔类型:

%t:打印true 或 false;

mygo基础

2.golang fmt格式“占位符”

print,printf,println:

函数 同函数输出多项 不同函数输出
Println 之间存在空格 换行
Print 不存在空格 不换行
Printf 格式化输出 不换行

占位符d、b、o、x、X==

package main
import "fmt"
import "os"
type point struct {
    a, b int
}
func main() {
    //Go 为常规 Go 值的格式化设计提供了多种打印方式。
    p := point{1, 2}
    fmt.Printf("%v\n", p) // {1 2}
    //如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名。
    fmt.Printf("%+v\n", p) // {x:1 y:2}
    //%#v 形式则输出这个值的 Go 语法表示。例如,值的运行源代码片段。
    fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
    //需要打印值的类型,使用 %T。
    fmt.Printf("%T\n", p) // main.point
    //格式化布尔值是简单的。
    fmt.Printf("%t\n", true)
    //格式化整形数有多种方式,使用 %d进行标准的十进制格式化。
    fmt.Printf("%d\n", 123)
    //这个输出二进制表示形式。
    fmt.Printf("%b\n", 14)
    //这个输出给定整数的对应字符。
    fmt.Printf("%c\n", 33)
    //%x 提供十六进制编码。
    fmt.Printf("%x\n", 456)
    //对于浮点型同样有很多的格式化选项。使用 %f 进行最基本的十进制格式化。
    fmt.Printf("%f\n", 78.9)
    //%e 和 %E 将浮点型格式化为(稍微有一点不同的)科学技科学记数法表示形式。
    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)
    //使用 %s 进行基本的字符串输出。
    fmt.Printf("%s\n", "\"string\"")
    //像 Go 源代码中那样带有双引号的输出,使用 %q。
    fmt.Printf("%q\n", "\"string\"")
    //和上面的整形数一样,%x 输出使用 base-16 编码的字符串,每个字节使用 2 个字符表示。
    fmt.Printf("%x\n", "hex this")
    //要输出一个指针的值,使用 %p。
    fmt.Printf("%p\n", &p)
    //当输出数字的时候,你将经常想要控制输出结果的宽度和精度,可以使用在 % 后面使用数字来控制输出宽度。默认结果使用右对齐并且通过空格来填充空白部分。
    fmt.Printf("|%6d|%6d|\n", 12, 345)
    //你也可以指定浮点型的输出宽度,同时也可以通过 宽度.精度 的语法来指定输出的精度。
    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
    //要最对齐,使用 - 标志。
    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
    //你也许也想控制字符串输出时的宽度,特别是要确保他们在类表格输出时的对齐。这是基本的右对齐宽度表示。
    fmt.Printf("|%6s|%6s|\n", "foo", "b")
    //要左对齐,和数字一样,使用 - 标志。
    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
    //到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字符串而不带任何输出。
    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)
    //你可以使用 Fprintf 来格式化并输出到 io.Writers而不是 os.Stdout。
    fmt.Fprintf(os.Stderr, "an %s\n", "error")
}

golang 的fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf。

# 定义示例类型和变量
type Human struct {
    Name string
}

var people = Human{Name:"zhangsan"}
普通占位符
占位符     说明                           举例                   输出
%v      相应值的默认格式。            Printf("%v", people)   {zhangsan}%+v     打印结构体时,会添加字段名     Printf("%+v", people)  {Name:zhangsan}
%#v     相应值的Go语法表示            Printf("#v", people)   main.Human{Name:"zhangsan"}
%T      相应值的类型的Go语法表示       Printf("%T", people)   main.Human
%%      字面上的百分号,并非值的占位符  Printf("%%")            %
布尔占位符
占位符       说明                举例                     输出
%t          truefalsePrintf("%t", true)       true
整数占位符
占位符     说明                                  举例                       输出
%b      二进制表示                             Printf("%b", 5)             101
%c      相应Unicode码点所表示的字符              Printf("%c", 0x4E2D)%d      十进制表示                             Printf("%d", 0x12)          18
%o      八进制表示                             Printf("%d", 10)            12
%q      单引号围绕的字符字面值,由Go语法安全地转义 Printf("%q", 0x4E2D)        '中'
%x      十六进制表示,字母形式为小写 a-f         Printf("%x", 13)             d
%X      十六进制表示,字母形式为大写 A-F         Printf("%x", 13)             D
%U      Unicode格式:U+1234,等同于 "U+%04X"   Printf("%U", 0x4E2D)         U+4E2D
浮点数和复数的组成部分(实部和虚部)
占位符     说明                              举例            输出
%b      无小数部分的,指数为二的幂的科学计数法,
        与 strconv.FormatFloat 的 'b' 转换格式一致。例如 -123456p-78
%e      科学计数法,例如 -1234.456e+78        Printf("%e", 10.2)     1.020000e+01
%E      科学计数法,例如 -1234.456E+78        Printf("%e", 10.2)     1.020000E+01
%f      有小数点而无指数,例如 123.456        Printf("%f", 10.2)     10.200000
%g      根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%g", 10.20)   10.2
%G      根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%G", 10.20+2i) (10.2+2i)
字符串与字节切片
占位符     说明                              举例                           输出
%s      输出字符串表示(string类型或[]byte)   Printf("%s", []byte("Go语言"))  Go语言
%q      双引号围绕的字符串,由Go语法安全地转义  Printf("%q", "Go语言")         "Go语言"
%x      十六进制,小写字母,每字节两个字符      Printf("%x", "golang")         676f6c616e67
%X      十六进制,大写字母,每字节两个字符      Printf("%X", "golang")         676F6C616E67
指针
占位符         说明                      举例                             输出
%p      十六进制表示,前缀 0x          Printf("%p", &people)             0x4f57f0
其它标记
占位符      说明                             举例          输出
+      总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符。 
                                           Printf("%+q", "中文")  "\u4e2d\u6587"
-      在右侧而非左侧填充空格(左对齐该区域)
#      备用格式:为八进制添加前导 0%#o)      Printf("%#U", '中')      U+4E2D
       为十六进制添加前导 0x(%#x)或 0X(%#X),为 %p(%#p)去掉前导 0x;
       如果可能的话,%q(%#q)会打印原始 (即反引号围绕的)字符串;
       如果是可打印字符,%U(%#U)会写出该字符的
       Unicode 编码形式(如字符 x 会被打印成 U+0078 'x')。
' '    (空格)为数值中省略的正负号留出空白(% d);
       以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开
0      填充前导的0而非空格;对于数字,这会将填充移到正负号之后

golang没有 ‘%u’ 点位符,若整数为无符号类型,默认就会被打印成无符号的。

宽度与精度的控制格式以Unicode码点为单位。宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。
操作数的类型为int时,宽度与精度都可用字符 ‘*’ 表示。

对于 %g/%G 而言,精度为所有数字的总数,例如:123.45,%.4g 会打印123.5,(而 %6.2f 会打印123.45)。

%e 和 %f 的默认精度为6

对大多数的数值类型而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。

而以字符串类型,精度为输出的最大字符数,如果必要的话会直接截断。

一.开发环境准备

1.go语言的强悍

属于编译型语言,执行顺序比解释型语言少一步,优于解释性语言

  1. 语法简单

    • 去掉了不需要的表达式括号
    • 循环也只有for循环一种方法,就可以实现数值、键值等各种遍历。
  2. 代码风格统一

    • 一套格式化工具——go fmt
    • 一些 Go 语言的开发环境或者编辑器在保存时,都会使用格式化工具进行修改代码的格式化,这样就保证了不同开发者提交的代码都是统一的格式。
  3. 开发效率高

    • Go语言实现了开发效率与执行效率的完美结合,让你像写Python代码(效率)一样编写C代码(性能)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f8mDduHR-1655435532034)(go基础.assets/image-20220615105857692.png)]

2.安装Go语言及搭建Go语言开发环境

2.1 GOROOT和GOPATH

GOROOTGOPATH都是环境变量,其中GOROOT是我们安装go开发包的路径,而从Go 1.8版本开始,Go开发包在安装完成后会为GOPATH设置一个默认目录,并且在Go1.14及之后的版本中启用了Go Module模式之后,不一定非要将代码写到GOPATH目录下,所以也就不需要我们再自己配置GOPATH了,使用默认的即可。

2.2 GOPROXY 非常重要

Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,也不再强制我们把代码必须写在GOPATH下面的src目录了,你可以在你电脑的任意位置编写go代码。(网上有些教程适用于1.11版本之前。)

默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct,由于国内访问不到https://proxy.golang.org,所以我们需要换一个PROXY,这里推荐使用https://goproxy.iohttps://goproxy.cn

可以执行下面的命令修改GOPROXY:

go env -w GOPROXY=https://goproxy.cn,direct

3.第一个go程序

初始化项目,生成go.mod文件

go mod init 项目名

创建main.go文件,编写main.go代码

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

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

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

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

在hello目录下执行:

go build

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

go build hello

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

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

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

c:\desktop\hello>hello.exe
Hello World!

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

go build -o heiheihei.exe
go run

go run main.go也可以执行程序,该命令本质上也是先编译再执行。

go install

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

4.依赖管理

4.1 为什么需要依赖管理

最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码

4.2 godep

godep是一个通过vender模式实现的Go语言的第三方依赖管理工具,类似的还有由社区维护准官方包管理工具dep

Go语言从v1.5开始开始引入vendor模式,如果项目目录下有vendor目录,那么go工具链会优先使用vendor内的包进行编译、测试等。

4.3 go module

自go1.11版本之后,go module将是go语言默认的依赖管理工具

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

4.3.1 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后就不必在gopath中创建项目了,并且能够很好的管理项目依赖的第三方包信息。具有模块化支持

4.4 go mod命令

常用的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         解释为什么需要依赖
4.5 go.mod

go.mod文件记录了项目所有的依赖信息,其结构大致如下:

module github.com/Q1mi/studygo/blogger

go 1.12

require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)

其中,

  • module用来定义包名
  • require用来定义依赖包及版本
  • indirect表示间接引用
4.6 依赖的版本

关于依赖的版本支持以下几种格式:

gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest
4.7 replace()

在国内访问golang.org/x的各个包都需要,你可以在go.mod中使用replace替换成github上对应的库。

replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)
4.8 go get(下载依赖包)

在项目中执行go get命令可以下载依赖包,并且还可以指定下载的版本。

  1. 运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  2. 运行go get -u=patch将会升级到最新的修订版本
  3. 运行go get package@version将会升级到指定的版本号version

如果下载所有依赖可以使用go mod download命令。

4.9 整理依赖

我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod文件中自动移除。这种情况下我们可以使用go mod tidy命令更新go.mod中的依赖关系。

4.10 go mod edit
  1. 格式化

    go mod edit -fmt
    
  2. 添加依赖

    go mod edit -require=golang.org/x/text
    
  3. 移除依赖

    如果只是想修改go.mod文件中的内容,那么可以运行go mod edit -droprequire=package path,比如要在go.mod中移除golang.org/x/text包,可以使用如下命令:

    go mod edit -droprequire=golang.org/x/text
    

    关于go mod edit的更多用法可以通过go help mod edit查看。

在项目中使用go module

既有项目

如果需要对一个已经存在的项目启用go module,可以按照以下步骤操作:

  1. 在项目目录下执行go mod init,生成一个go.mod文件。
  2. 执行go get,查找并记录当前项目的依赖,同时生成一个go.sum记录每个依赖库的版本和哈希值。
新项目

对于一个新创建的项目,我们可以在项目文件夹下按照以下步骤操作:

  1. 执行go mod init 项目名命令,在当前项目文件夹下创建一个go.mod文件。
  2. 手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖。

5.如何使用go module导入本地包

其中,

  • module用来定义包名
  • require用来定义依赖包及版本
  • indirect表示间接引用
  • replace代替跳墙

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。到今天Go1.14版本推出之后Go modules 功能已经被正式推荐在生产环境下使用了。

5.1 同一个项目下

假设我们现在有文件目录结构如下:

└── bubble
    ├── dao
    │   └── mysql.go
    ├── go.mod
    └── main.go

其中bubble/go.mod内容如下:

module github.com/q1mi/bubble

go 1.14

bubble/dao/mysql.go内容如下:

package dao

import "fmt"

func New(){
	fmt.Println("mypackage.New")
}

bubble/main.go内容如下:

package main

import (
	"fmt"
	"github.com/q1mi/bubble/dao"
)
func main() {
	dao.New()
	fmt.Println("main")
}
5.2 不同项目下

我们现在有文件目录结构如下:

├── p1
│   ├── go.mod
│   └── main.go
└── p2
    ├── go.mod
    └── p2.go

p1/main.go中想要导入p2.go中定义的函数。

p2/go.mod内容如下:

module liwenzhou.com/q1mi/p2

go 1.14

p2/p2.go内容如下:

package p2

import "fmt"

func New() {
	fmt.Println("mypackage.New")
}

p1/main.go中按如下方式导入

package main

import (
	"fmt"
	"liwenzhou.com/q1mi/p2"
)

func main() {
	p2.New()
	fmt.Println("main")
}

因为我并没有把liwenzhou.com/q1mi/p2这个包上传到liwenzhou.com这个网站,我们只是想导入本地的包,这个时候就需要用到replace这个指令了。

p1/go.mod内容如下:

module github.com/q1mi/p1

go 1.14


require "liwenzhou.com/q1mi/p2" v0.0.0
replace "liwenzhou.com/q1mi/p2" => "../p2"

二、go基础

一、Go语言基础之变量和常量

1.标识符与关键字

1.1 标识符

Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。

1.2 关键字

25个关键字,37个保留字

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

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

2.变量

2.1 变量的来历

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

2.2 变量类型
  • 常见的数据类型:整型、浮点型、布尔型等。
  • 变量必须经过声明才能开始使用。
2.3 声明变量

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

声明格式:

var 变量名  变量类型

批量声明

var (
	a String
	b int
	c bool 
	d float32
)
2.4 变量的初始化
  1. Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。
  2. 每个变量会被初始化成其类型的默认值
2.4.1 变量初始化标准格式
var 变量名  类型  =  表达式

举例:一次初始化多个变量

var name,age = "Q1mi",20
2.4.2 类型推导

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

var name = "Q1mi"
var age = 18
2.4.3 短变量声明

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

package main

import (
	"fmt"
)
// 全局变量m
var m = 100

func main() {
	n := 10
	m := 200 // 此处声明局部变量m
	fmt.Println(m, n)
}
2.4.4 匿名变量

在多重赋值时如果要忽略某个值,可以使用匿名变量,用_表示

如:

func foo() (int,string) {
 	return 10,"Q1mi"
}

func main() {
	x,_ := foo()
	_,y := foo()
	fmt.Println("x=",x)
	fmt.Println("y=",y)
}

匿名变量不占用内存空间,所以也不存在重复声明

注意事项:

  1. 函数外的每个语句都必须以关键字开始(var const func 等)
  2. :=不能用于函数之外
  3. _多用于占位,表示忽略值
2.5 变量默认值
  1. 整型和浮点型变量的默认值为0
  2. 字符串变量的默认值为空字符串
  3. 布尔型变量默认为false
  4. 切片、函数、指针变量的默认为nil

3.常量

对于变量来说,常量是恒定不变的值

多个常量也可以一起声明

const (
	pi  = 3.1415
	e   =  2.718
)

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同

const (
	n1 = 100
	n2
	n3
)
//n1=n2=n3=100
3.1 iota

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

  • iota在const关键字出现时即被重置为0
  • const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
  • 使用iota能简化定义,在定义枚举时很有用。
基础例子
const (
		n1 = iota //0
		n2        //1
		n3        //2
		n4        //3
	)
使用_跳过某些值
const (
	n1 = iota   //0
    n2			//1
    _
    n3			//3
)
iota声明中间插队
const (
		n1 = iota //0
		n2 = 100  //100
		n3 = iota //2
		n4        //3
	)
	const n5 = iota //0
多个iota定义在一行
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语言基础之基本数据类型

1.数据类型

  1. 基本数据类型(7个)

    • ​ 整型、浮点型、布尔型、字符串、复数、byte、rune
  2. 引用数据类型(6个)

    • ​ 指针、切片(slice)、map、通道(channel)、接口和函数
  3. 复合数据类型

    • ​ 数组、结构体

1.1 整型

两大类

  • 按长度分为: int8、 intl16、 int36、 int64
  • 对应的无符号:uint8、uintl16、uint32、uint64

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

类型 描述
uint8 无符号 8位整型 (0 到 255)
uint16 无符号 16位整型 (0 到 65535)
uint32 无符号 32位整型 (0 到 4294967295)
uint64 无符号 64位整型 (0 到 18446744073709551615)
int8 有符号 8位整型 (-128 到 127)
int16 有符号 16位整型 (-32768 到 32767)
int32 有符号 32位整型 (-2147483648 到 2147483647)
int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型

类型 描述
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

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

数字字面量语法

  • Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字
  • 而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 表示 v 的值等于 123456。

1.2 浮点型

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

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

package main

import (
	"fmt"
	"math"
)

func main() {
	var a = math.MaxFloat32
	fmt.Printf("%f\n", math.Pi)
	fmt.Printf("%.2f\n", math.Pi)
	fmt.Println(a)
}

1.3 复数

complex64和complex128

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

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

1.4 布尔值

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

注意:

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

1.5 字符串

  1. go语言中的字符串以原生数据类型出现(int bool float32 float64)
  2. 内部实现使用UTF-8编码
  3. 可以在go语言的源码中直接添加非ASCII码字符
1.5.1 字符串转义符
转义符 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\\ 反斜杠
1.5.2 多行字符串

不许使用反引号字符,如

s1 := `第一行
第二行
第三行
`
fmt.Println(s1)
1.5.3 字符串常用操作
方法 介绍
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作

1.6 byte和rune类型

字符用单引号(’)包裹起来

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

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

  1. uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
  2. rune类型,代表一个 UTF-8字符

当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32

修改字符串

要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

package main

import "fmt"

func main() {
	traversalString()
	changeString()
}

//遍历字符串
func traversalString() {
	s := "hello沙河"
	for i := 0; i < len(s); i++ {
		fmt.Printf("%v(%c)", s[i], s[i])
	}
	fmt.Println()
	for _, r := range s {
		fmt.Printf("%v(%c)", r, r)
	}
	fmt.Println()
}
//修改字符串
func changeString() {
	s1 := "big"
	//强制类型转换
	byteS1 := []byte(s1)
	byteS1[0] = 'p'
	fmt.Println(string(byteS1))

	s2 := "白萝卜"
	runeS2 := []rune(s2)
	runeS2[0] = '红'
	fmt.Println(string(runeS2))
}

2.类型转换

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

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

T(表达式)

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

编写代码统计出字符串"hello沙河小王子"中汉字的数量。

//统计编写代码统计出字符串"hello沙河小王子"中汉字的数量。
func num() {
	s := "hello沙河小王子"
	a := 0
	for _, r := range s {
		if r > 'z' {
			a++
		}
	}
	fmt.Println(a)
}

三、go语言之运算符

Go 语言内置的运算符有:

  1. 算术运算符
  2. 关系运算符
  3. 逻辑运算符
  4. 位运算符
  5. 赋值运算符

https://www.liwenzhou.com/posts/Go/03_operators/

四、go语言基础之流程控制

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

Go语言基础之流程控制 | 李文周的博客 (liwenzhou.com)

1. if else(分支结构)

1.1 if条件判断
if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else{
    分支3
}
1.2 if条件的特殊写法

if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,举个例子:

func ifDemo2() {
	if score := 65; score >= 90 {
		fmt.Println("A")
	} else if score > 75 {
		fmt.Println("B")
	} else {
		fmt.Println("C")
	}
}

2. for(循环结构)

2.1 for基础

for循环的基本格式如下:

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

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循环可以通过breakgotoreturnpanic语句强制退出循环。

2.2 for range(键值循环)

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

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

3.switch case语句

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

func switchDemo1() {

	finger := 3
	switch finger {
	case 1:
		fmt.Println("大拇指")
	case 2:
		fmt.Println("食指")
	case 3:
		fmt.Println("中指")
	default:
		fmt.Println("无效的输入")
	}
}

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

func testSwitch3() {
    switch n:=7;n {
        case 1,3,6,8,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

4.goto(跳转到指定标签)

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

//不使用goto
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)
		}
		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循环")
}

5.break跳出循环

break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的forswitchselect的代码块上。 举个例子:

func breakDemo1() {
BREAKDEMO1:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				break BREAKDEMO1
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	fmt.Println("...")
}

6.continue继续下次循环

continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。

continue语句后添加标签时,表示开始标签对应的循环。例如:

func continueDemo() {
forloop1:
	for i := 0; i < 5; i++ {
		// forloop2:
		for j := 0; j < 5; j++ {
			if i == 2 && j == 2 {
				continue forloop1
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
}

五、Go语言基础之数组

1.数组定义

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

2.数组的初始化

2.1 方法一

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

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)                      //[北京 上海 深圳]
}
2.2 方法二

上面那种方法每次都要给定一个确定的长度,下面我们让编译器自行推断数组的长度

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
}
2.3 方法三

使用指定索引值的方式来初始化数组

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
}

3.数组的遍历

func main() {
	var a = [...]string{"北京", "上海", "深圳"}

	//方法一遍历
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}
	//方法二  for range遍历
	for index, value := range a {
		fmt.Println(index, value)
	}
}

4.多维数组

func main() {
   a := [3][2]string{
      {"北京", "上海"},
      {"广州", "深圳"},
      {"成都", "重庆"},
   }
   for _, v1 := range a {
      for _, v2 := range v1 {
         fmt.Printf("%s\t", v2)
      }
      fmt.Println()
   }
}

注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。例如:

//支持的写法
a := [...][2]string{
	{"北京", "上海"},
	{"广州", "深圳"},
	{"成都", "重庆"},
}

5.数组是值类型

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

func main() {
   a := [3]int{10, 20, 30}
   modifyArray(a)
   fmt.Println(a)
   b := [3][2]int{
      {1, 1},
      {1, 1},
      {1, 1},
   }
   modifyArray2(b)
   fmt.Println(b)
}

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

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

输出:

[10 20 30]
[[1 1] [1 1] [1 1]]

注意:

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

六、Go语言基础之切片

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

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

1.切片的定义

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

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比较
}
1.1 切片的长度和容量

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

1.2 切片表达式
  • 切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。
  • 它有两种变体:
    • 一种指定low和high两个索引界限值的简单的形式,
    • 另一种是除了low和high索引界限值外还指定容量的完整的形式。
  • 切片元素数量(len)=high-low
  • 切片容量(cap)=length-low

简单切片表达式

  1. 切片底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。
  2. 切片表达式中的lowhigh表示一个索引范围(左包含,右不包含)
  3. 下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low
  4. 容量等于得到的切片的底层数组的容量(length-low)。
func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}

输出:

s:[2 3] len(s):2 cap(s):4

对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。

完整切片表达式

对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

a[low : high : max]
  • 面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。
  • 它会将得到的结果切片的容量设置为max-low
  • 在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

1.3 使用make()函数构造切片

如果不基于数组创建切片,我们需要动态创建一个切片,我们需要使用内置的make()函数

make([]T, size, cap)
  • T:切片的元素类型
  • size:切片中元素的数量(len)
  • cap:切片的容量(cap)
1.4 切片的本质

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

  1. 底层数组的指针
  2. 切片的长度(len)
  3. 切片的容量(cap)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E6P6ZIdF-1655688771942)(go基础.assets/image-20220617203649168.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jx8xMJB4-1655688771943)(go基础.assets/image-20220617203711239.png)]

1.5 判断切片是否为空

要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

2.切片不能直接比较

  1. 切片不能使用==来比较两个切片是否含有全部相等的元素
  2. 切片唯一合法的比较是和 nil
  3. 值为nil的切片没有底层数组,一个值为nil的切片长度和容量都是0
  4. 我们不能说一个长度和容量都是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

3.切片的赋值拷贝

切片拷贝后生成的新的切片和原来的切片共享同一个底层数组,对一个切片的修改会影响另一个切片的内容

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

可以使用copy() 函数复制切片 解决这一问题

4.切片的遍历

和数组一致,支持索引遍历和for range遍历

func main() {
   s := []int{1, 3, 5}
   for i := 0; i < len(s); i++ {
      fmt.Println(i, s[i])
   }
   fmt.Println("=======")
   for index, value := range s {
      fmt.Println(index, value)
   }
}

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

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7]
}

注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

6.切片的扩容策略

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

//新申请的容量   cap
//旧容量        old.cap
//最终容量      newcap

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

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

  1. 如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
  2. 如果新申请容量(cap)小于2倍的旧容量(old.cap)
    1. 如果旧切片的长度小于1024,最终容量就是旧容量的2倍,即 newcap = doublecap
    2. 如果旧切片的长度大于1024,
      • 最终容量从旧容量开始循环增加原来的1/4,即 (newcap=old.cap,for {newcap += newcap/4}),直到最终容量大于等于新申请的容量
      • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

7.使用copy()函数复制切片

由于切片是引用类型,所以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]
}

8.从切片中删除元素

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

七、Go语言基础之map

Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。

1.map定义

Go语言中 map的定义语法如下:

map[KeyType]ValueType

其中

  • KeyType代表键的类型
  • ValueType代表值的类型

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

make(map[KeyType]ValueType,[cap])  //cap表示map的容量

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

2.map基本使用

func main() {
	scoreMap := make(map[string]int, 8)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	fmt.Println(scoreMap)
	fmt.Println(scoreMap["小明"])
	fmt.Printf("type of a:%T\n", scoreMap)
}

map也支持在声明的时候填充元素,例如:

func main() {
	userInfo := map[string]string{
		"username": "沙河小王子",
		"password": "123456",
	}
	fmt.Println(userInfo) //
}

3.判断某个键是否存在

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

value,ok :=map[key]

举个例子:

func main() {
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
	v, ok := scoreMap["张三"]
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("查无此人")
	}
}

4.map遍历

go语言中使用for range遍历map

func main() {
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	scoreMap["娜扎"] = 60
	for key,value := range scoreMap {
		fmt.println(key,value)
	}
}	

只想遍历key的时候按照下面的写法

func main() {
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	scoreMap["娜扎"] = 60
	for key := range scoreMap{
		fmt.Println(k)
	}
}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

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

使用delete()内建函数从map中删除一组键值对,delete()函数格式如下

delete(map,key)

其中,

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

如:

func main() {
	scoreMap := map[string]int{
		"张三":90,
		"小明":100,
		"娜扎":60,
	}
	delete(scoreMap,"娜扎")
	for k,v := range scoreMap{
		fmt.Println(k,v)
	}
}

6.按照指定顺序遍历map

不加随机种子,每次遍历获取都是重复的一些随机数据

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)
		value := rand.Intn(100)
		scoreMap[key] = value
	}
	//取出map中所有的key,存入切片keys中
	var keys = make([]string,0,200)
	for key := range scoreMap {
		keys = append(keys,key)
	}
	//对切片进行排序
	for _,key := range keys {
		fmt.Println(key,scoreMap[key])
	}
	
}

7.元素为map类型的切片

func main() {
	var mapSlice = make([]map[string]string,3)
	for index,value := range mapSlice {
		fmt.Printf("index:%d value:%v\n",index,value)
	}
	fmt.Println("after init")
	//对切片中的元素进行初始化
	mapSlice[0] = make(map[string]string,10)
	mapSlice[0]["name"] = "小王子"
	mapSlice[0]["password"] = "123456"
	mapSlice[0]["address"] = "沙河"
	for index,value := range mapSlice {
		fmt.Printf("index:%d value:%v\n",index,value)
	}
}

输出:

index:0 value:map[]
index:1 value:map[]                                        
index:2 value:map[]                                        
after init                                                 
index:0 value:map[address:沙河 name:小王子 password:123456]
index:1 value:map[]                                        
index:2 value:map[]    

7.值为切片类型的map

//二维数组
func main() {
	var sliceMap = make(map[string][]string, 3)
	fmt.Println(sliceMap)
	fmt.Println("after init")
	key := "中国"
	value, ok := sliceMap[key]
	if !ok {
		value = make([]string, 0, 2)
	}
	value = append(value, "北京", "上海")
	sliceMap[key] = value
	fmt.Println(sliceMap)
}

八、go语言基础之函数

1.函数定义

定义函数使用func关键字,格式如下

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

其中,

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

八、go语言基础之函数

1.函数

1.函数定义

定义函数使用func关键字,格式如下

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

其中,

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

例如两数求和:

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

既不需要参数也不需要返回值:

func sayHello() {
	fmt.Println("Hello 沙河")
}

2.函数的调用

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

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

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

3.参数

3.1 类型简写

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

func intSum(x,y int) int {
	return x+y
}
3.2 可变参数

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

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

func intSum2(x ...int) int {
	fmt.Println(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

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

4.返回值

4.1 多返回值

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

func calc(x,y int) (int,int) {
	sum := x+y
	sub := x-y
	return sum,sub
}
4.2 返回值命名

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

func calc(x,y int) (sum,sub int) {
	sum = x+y
	sub = x-y
	return
}
4.3 返回值补充

当我们的返回值类型为slice时,nil可以看作一个有效的spice,没有必要显示返回一个长度为0的切片。

func someFunc(x string) []int {
	if x=="" {
		return nil  //没有必要返回[]int {}
	}
}

2.函数进阶

1.变量作用域

1.1 全局变量

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

1.2 局部变量

局部变量又分为两种:

  1. 函数内定义的变量无法在该函数外使用
  2. 如果局部变量和全局变量重名,优先访问局部变量。

2.函数类型与变量

2.1 定义函数类型

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
2.2 函数类型变量

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

func main() {
	type calculation func(int, int) int
	var c calculation
	c = add
	fmt.Printf("type of c:%T\n", c)
	fmt.Println(1, 2)

	f := add
	fmt.Printf("type of f:%T\n", f)
	fmt.Println(f(10, 20))
}

func add(x, y int) int {
	return x + y
}
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
}

3.高阶函数

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

3.1 函数作为参数
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
}
3.2 函数作为返回值
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
	}
}

4.匿名函数和闭包

4.1 匿名函数

匿名函数就是没有函数名的函数,格式如下

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

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

func main() {
	//将匿名函数保存到变量
	add := func(x,y int) {
		fmt.Println(x+y)
	}
	add(10,20)
	
	//自执行函数:匿名函数定义完加()直接执行
	func(x,y int) {
		fmt.Println(x+y)
	}(10,30)
}

匿名函数多用于实现回调函数和闭包。

4.2 闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。换句话:闭包=函数+引用环境。例如

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 {
	fmt.Println(suffix) //.jpg    .txt  
	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
}

5.defer语句

  1. defer语句会将其后面跟随的语句进行延迟处理。
  2. defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行
  3. 也就是说,先被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语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

5.1 defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为

  1. 给返回值赋值
  2. RET指令两步。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LslSekP9-1656895196619)(go基础.assets/image-20220620162920004.png)]

5.2 defer经典案例

Go语言基础之函数 | 李文周的博客 (liwenzhou.com)

没看懂

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())  //5
	fmt.Println(f2())  //6
	fmt.Println(f3())  //5
	fmt.Println(f4())  //5
}

3.内置函数

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

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

注意:

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

九、go语言基础之指针

数据在内存中的地址就是指针

  1. 要明白go语言的指针要先明白三个概念:指针地址、指针类型和指针取值
  2. 任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
  3. Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。

1.指针地址和指针类型

  1. 每个变量在运行时都有一个地址,这个地址代表变量在内存中的位置。
  2. Go语言中使用**&**字符放在变量前面对变量进行“取地址”操作。
  3. 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的图示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wm1LS6Pi-1656895196622)(go基础.assets/image-20220621143704827.png)]

2.指针取值

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

指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值

func main() {
	//指针取值
	a := 10
	b := &a //取变量a的地址,将指针保存到b中
	fmt.Printf("type of b:%T\n",b)
	c:=*b  //指针取值,根据内存地址取值===>取到a的值10
	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

**总结:*取地址操作符&和取值操作符 是一对互补的操作,&取出地址,*根据地址取出地址指向的值

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

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

指针传值示列:

没懂

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
}

3.new和make

//引用类型的没有分配内存,报异常  panic
func main() {
	var a *int
	//a = new(int)
	*a = 100
	fmt.Println(*a)

	var b map[string]int
	//b = make(map[string]int, 10)
	b["沙河娜扎"] = 100
	fmt.Println(b)
}

执行上面的代码会引发panic?

  1. 在Go语言中对于引用类型的变量,在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。
  2. 对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。
  3. Go语言中new和make是内建的两个函数,主要用来分配内存。作用:初始化并分配内存地址

3.1 new

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

func new (Type) *Type

其中,

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值,举例:

func main () {
	//a  b属于指针类型
	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)
}

3.2 make

  1. make也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建
  2. 返回的类型就是这三个类型本身,而不是他们的指针类型
func make(t Type,size ...IntegerType) Type

make函数是无可替代的,我们使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对他们进行操作

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

func main() {
	var b map[string]int
	b = make(map[string]int, 10)
	b["沙河娜扎"] = 100
	fmt.Println(b)
}

3.3 new和make的区别

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

十、go语言基础之结构体

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

1.类型别名和自定义类型

1.1 自定义类型

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

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

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

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

1.2 类型别名

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

type TypeAlias = type

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

type byte = uint8
type rune = int32

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

类型别名与类型定义表面上看只有一个等号的差异,一个是定义了新的类型,一个是给原来类型起个名字。下面代码来区别

//类型定义
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(类型别名)的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

2.结构体

  1. go语言中的基础数据类型可以表示一些事物的基本属性,但当我们想表达一个事务的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了
  2. go提供一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名struct。
  3. 也就是我们可以通过struct来定义自己的类型。go通过struct来实现面向对象

2.1 构体的定义

使用typestruct关键字来定义结构体。具体代码:

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

其中,

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

举例,定义一个Person(人)结构体,代码:

type Person struct {
	name string
	city string
	age int8
}

同样类型的字段也可以在写一行

type Person1 struct {
	name,city   string
	age         int8
}

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

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

2.2 结构体实例化

  1. 只有当结构体实例化时,才会真真的分配内存。也就是必须实例化后才能使用 结构体字段
  2. 结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型
var 结构体实例  结构体类型
2.2.1 基本实例化
type Person struct {
	name string
	city string
	age  int8
}

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

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

2.2.2 匿名结构体

函数内部

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

输出

struct { Name string; Age int }{Name:"小王子", Age:18}
2.2.3 创建指针类型结构体

我们还可以通过使用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 = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n",p2)   //p2=&main.person{name:"小王子",city:"上海",age:28}
2.2.4 取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次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语言帮我们实现的语法糖。

3.结构体初始化

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

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

3.1 使用键值对初始化

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

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

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

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

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

p7 := &person {
	city := "北京"
}

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

3.2 使用值的列表初始化

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

p8 := &person{
	"沙河娜扎",
	"北京",
	28,
}
fmt.Printf("p8=%#v\n",p8)

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

  1. 必须初始化结构的所有字段
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致
  3. 该方式不能和键值初始化方式混用

4.结构体内存布局

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

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

4.1 空结构体

空结构体是不占用空间的

var  v struct{}
fmt.Println(unsafe.Sizeof(v))  //0

5.面试题

type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "小王子", age: 18},
		{name: "娜扎", age: 23},
		{name: "大王八", age: 9000},
	}

	fmt.Println(stus)
	for _, stu := range stus {
		m[stu.name] = &stu
		fmt.Println(stu)
	}
	fmt.Println(m)
	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}

输出:

[{小王子 18} {娜扎 23} {大王八 9000}]
{小王子 18}                                                   
{娜扎 23}                                                     
{大王八 9000}                                                 
map[大王八:0xc0000040d8 娜扎:0xc0000040d8 小王子:0xc0000040d8]
娜扎 => 大王八                                                
大王八 => 大王八                                              
小王子 => 大王八

6.构造函数

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

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

调用构造函数

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

7.方法和接收者

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

方法定义格式如下:

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

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为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("小王子",27)
	p1.Dream()
}

没懂 函数和方法的区别:方法属于特定类型,函数不属于任何类型

7.1 指针类型的接收者

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

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

调用该方法

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

7.2 值类型的接收者

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

//SetAge2  设置p的年龄
// 使用值接收者
func (p Person) setAge2(newAge int8) {
	p.age = newAge
}
func main() {
	p1 := NewPerson("小艾王子",25)
	p1.Dream()
	fmt.Println(p1.age)  //25
	p1.SetAge2(30)  //(*p1).SetAge2(30)
	fmt.Println(p1.age)  //25
}

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

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

8.任意类型添加方法

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

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

9.结构体的匿名字段

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

//Person 结构体Person类型
type Person struct {
	string
	int
}
func main() {
	p1 := Person {
		"小王子",
		18,
	}
	fmt.Printf("%#v\n",p1) 			//main.Person{string:"北京", int:18}
	fmt.Println(p1.string,p1.int)	//北京 18
}

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

10.嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针,例如:

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

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

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

10.1 嵌套匿名字段

上面user结构体中嵌套的Adress结构体也可同意采用匿名字段的方式

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

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

func main() {
	var user2 User
	user2.Name = "小王子"
	user2.Gender = "男"
	user2.Address.Province = "山东"    // 匿名字段默认使用类型名作为字段名
	user2.City = "威海"                // 匿名字段可以省略
	fmt.Printf("user2=%#v\n", user2)
    //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

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

10.2 嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。 在这种情况下,为了避免歧义需要通过指定具体的内嵌结构体字段名

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

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

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

func main() {
	var user3 User3
	user3.Name = "沙河娜扎"
	user3.Gender = "男"
	// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
	user3.Address3.CreateTime = "200000" //指定Address3结构体中的CreateTime
	user3.Email.CreateTime = "2000"      //指定Email结构体中的CreateTime
	fmt.Printf("user2=%#v\n", user3)
	//user2=main.User3{Name:"沙河娜扎", Gender:"男", Address3:main.Address3{Province:"", City:"", CreateTime:"200000"}, Email:main.Email{Account:"", CreateTime:"2000"}}

}

11.结构体的“继承”

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

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

func (a *Animal) move() {
	fmt.Printf("%会动:\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()  //乐乐会动
}

12.结构体字段的可见性

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

13.结构体与JSON序列化

JSON是一种轻量级的数据交换格式。易于让人阅读和编写,同时也易于机器解析和生成。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"
		Student: 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 marchal 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)
}

14.结构体标签(Tag)

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

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

结构体tag由一个或多个键值对组成,健与值使用冒号分割,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

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

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

//Student 学生
type Student struct {
	ID  int  `json:"id"`
	Gender  string
	name    string
}

func main() {
	s1 := Student {
		ID:  1,
		Gender:"男",
		name: "沙河娜扎",
	}
	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":"男"}
}

15.结构体和方法补充知识点

因为slice和map这两种数据类型都包含了指向底层数组数据的指针,因此我们在需要复制它们时要特别注意。

type Person struct {
	name  string
	age   int8
	dreams [] string
}

func (p *Person) SetDreams(dreams [] string) {
	p.dreams = dreams
}

func main() {
	p1 := Person{name: "小王子", age: 18}
	data := []string{"吃饭", "睡觉", "打豆豆"}
	p1.SetDreams(data)
	
	// 你真的想要修改 p1.dreams 吗?
	data[1] = "不睡觉"
	fmt.Println(p1.dreams)  // ?

}

正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。

func (p *Person) SetDreams(dreams []string) {
	p.dreams = make([]string, len(dreams))
	copy(p.dreams, dreams)
}

同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。

十一、go语言基础之包

在工程化的go语言开发项目中,go语言的源码复用建立在包(package)基础之上。下面介绍如何定义包、导出包的内容导入其他包

包与依赖管理

1.包

1.1 包介绍

go语言中支持模块化的开发理念, 在go语言中使用包(package)来支持代码模块化和代码复用。一个包是由一个或多个go源码文件(.go结尾的文件)组成,是一种高级的代码复用方案,go语言为我们提供了很多内置包,如fmt、os、io。

例如,在之前的章节中我们频繁使用了fmt这个内置包。

package main

import "fmt"

func main() {
	fmt.Println("Hello world!")
}

上面短短的几行代码涉及到了如何定义包及如何引用其他包两个内容。

1.2 定义包

我们可以根据自己需要创建自定义包。一个包可以简单理解为一个存放.go文件的文件夹。该文件夹下面的所有.go文件都要在非注释的第一行添加如下声明,声明该文件归属的包

package packagename

其中,

  • package:声明包的关键字
  • packagename:包名,可以不与文件夹的名称一致,不能包含-符号,最好与实现的功能相对应

另外需要注意一个文件夹下面直接包含的文件夹只能归属一个包,同一个包文件不能在多个文件夹下。包名为==main的包是应用程序的入口包,这种包编译后会得到一个可执行文件==,而编译不包含main包的源代码则不会得到可执行文件。

1.3 标识可见性

在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀。例如fmt.Println("Hello world!"·,就是指调用fmt包中的Println函数。

如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在go语言中是通过标识符的首字母大小写来控制标识符的对外可见(public)不可见(private)的在一个包内部只有首字母大写的标识符才是对外可见的。

例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。

package demo

import "fmt"

//包级别标识符可见性

//num 定义一个全局整型变量
//首字母小写,对外不可见(只能在当前包内使用)
var num = 100

//Mode 定义一个常量
//首字母大写,对外不可见(可在其他包中使用)
const Mode=1
//person 定义一个代表人的结构体
//首字母小写,对外不可见(只能在当前包内使用)
type person struct {
	name string
	Age int
}

//Add 返回两个整数和的函数
//首字母大写,对外可见(可在其他包中使用)
func Add(x,y int)int {
	return x+y
}
//sayHi 打招呼的函数
//首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
	var myName = "七米"
	fmt.Println(myName)
}

同样的规则也适用于结构体,结构体中可导出字段的字段名称必须首字母大写。

type Student struct {
	Name  string // 可在包外访问的方法
	class string // 仅限包内访问的字段
}

1.4 包引入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uKQJhOrT-1656895196624)(go基础.assets/image-20220627173154569.png)]

要在当前包中使用另一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整引入声明语句格式如下:

import importname "path/to/package"

其中,

  • importname:引入的包名,通常都省略。默认值为引入包的包名
  • path/to/package: 引入包的路径名称,必须使用双引号包裹起来
  • go语言中禁止循环导入包

一个go源码文件中可以同时引入多个包,例如:

import "fmt"
import "net/http"
import "os"

当然可以使用批量引入的方式

import (
	"fmt"
	"net/http"
	"os"
)

当引入的多个包中存在相同的包名或者想自行为某个引入的包设置一个新包名时,都需要通过importname指定一个在当前文件夹中使用的新包名。例如,在引入fmt包时为其指定一个新包名f

import f "fmt"

这样在当前这个文件夹中就可以通过使用f来调用fmt包中的函数了

f.Println("Hello world")

如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要时为了加载这个包,从而使得这个包中的资源得以初始化。被匿名引入的包中init函数将被执行且执行一遍。

import _"github.com/go-sql-driver/mysql"

匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。

需要注意的时,go语言中不允许引入包却不在代码中使用这个包的内容,如果引入了未使用的包则会触发编译错误。

1.5 init初始化函数

在每一个go源文件中,都可以定义任意个如下格式的特殊函数:

func main() {
	//...
}
  • init函数在包导入的时候自动执行
  • 没有参数也没有返回值

这种特殊的函数不接受任何参数也没有任何返回值,我们不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行。

一个包的初始化过程是按照代码中引入的顺序来执行的,所有在该包中声明的init函数都将被串行调用并且调用执行一次每一个包初始化的时候都是先执行依赖的包中声明的init函数在执行当前包中声明的init函数,确保在程序的main函数开始执行时所有的依赖包都已经初始化完成。

go基础第一遍学习(李文周博客)_第1张图片

总结

  1. 每一个包的初始化是先从初始化包级别变量开始的。
  2. go语言中也是不允许循环引用的——a里面引用b然后b里面引用a
  3. 不允许导入包而不使用
  4. 同一个包中的不同文件可以直接调用(不需要写包名,直接写方法名、变量名)
    • 寻找的时候现在自己包里找,找不到在去兄弟包中找
  5. 想要自己的变量在其他包中使用,必须首字母大写,首字母小写的话别的包是使用不到的
  6. 单行导入,多行导入。导入包重名,起别名

从下面的示例中我们就可以看出包级别变量的初始化会先于init初始化函数。

package main

import "fmt"

var x int8 = 10

const pi = 3.14

func init() {
	fmt.Println("x:",x)
	fmt.Println("pi:",pi)
	sayHi()
}

func sayHi() {
	fmt.Println("Hello world")
}

func main() {
	fmt.Println("你好,世界")
}

2.go module

  1. 在go的早期版本中,我们编写go项目代码时所依赖的所有第三方包都需要保存在gopath这个目录下面。
  2. 这样的依赖管理不支持版本管理,同一个依赖包只能存在一个版本的代码。
  3. 可是我们本地的多个项目完全可能分别依赖同一个第三方包的不同版本。

2.1 go module介绍

go module是go1.11版本发布的依赖管理方案,从go 1.14版本开始推荐在生产环境使用,于go 1.16版本默认开启。

go module相关命令

命令 介绍
go mod init 初始化项目依赖,生成go.mod文件
go mod download 根据go.mod文件下载依赖
go mod tidy 比对项目文件中引入的依赖与go.mod进行比对
go mod graph 输出依赖关系图
go mod edit 编辑go.mod文件
go mod vendor 将项目的所有依赖导出至vendor目录
go mod verify 检验一个依赖包是否被篡改过
go mod why 解释为什么需要某个依赖

go语言在go module的过渡阶段提供了GO11MODULE这个环境变量来作为是否启用go module功能的开关,go 1.16之后go module已经默认开启。

GOPROXY

  1. 这个环境变量主要是用于设置go模块代理,
  2. 用于使go在后续拉取模块版本时能够脱离传统的vcs方式
  3. 直接通过镜像站点来快速拉取

GOPROXY 的默认值是:https://proxy.golang.org,direct,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个https://goproxy.cnhttps://goproxy.io,当然如果你的公司有提供GOPROXY地址那么就直接使用。设置GOPAROXY的命令如下:

go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。

GOPRIVATE

设置了GOPROXY之后,go命令就会从配置的代理地址拉取和效验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或github私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置GOPEIVATE环境变量。GOPRIVATE用来告诉go命令哪些仓库属于私有仓库,不必通过代理服务器拉取和效验。

GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:

$ go env -w GOPRIVATE="git.mycompany.com"

这样在拉取以git.mycompany.com为路径前缀的依赖包时就能正常拉取了。

此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none,允许通内部代理拉取私有仓库的包。

2.2 使用go module引入包

Go语言基础之包 | 李文周的博客 (liwenzhou.com)

十二、Go语言基础之接口

Go语言基础之接口 | 李文周的博客 (liwenzhou.com)

  1. 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
  2. 接口是一种类型,一种抽象的类型

1.1 为什么要使用接口

package main

import "fmt"

// 为什么需要接口

type dog struct{}

//定义一个方法  方法区别于函数,要有一个参数的接收者
func (d dog) say() {
	fmt.Println("汪汪哇")
}

type cat struct{}

func (c cat) say() {
	fmt.Println("喵喵喵喵")
}

//定义一个抽象的类型,只要实现了say这个方法类型都可以称为sayer类型
type sayer interface {
	say()
}

type person struct {
	name string
}

func (p person)say()  {
	fmt.Println("啊啊啊啊")
}


//接口不管你是什么 只管你要实现什么方法
//打的函数   接收一个参数,不管是什么类型
func da(arg sayer) {
	arg.say()
}

func main() {
	c1 := cat{}
	da(c1)
	d1 := dog{}
	da(d1)
	p1:=person{
		name:"娜扎",
	}
	da(p1)
}

1.2 使用值接收和使用指针接收的区别

  1. 使用值接收者实现接口:类型的值和类型的指针,都能够保存到接口变量中
  2. 使用指针接收者实现接口:只有类型指针能够保存到接口变量中
package main

import "fmt"

type mover interface {
	move()
}

type person struct {
	name string
	age  int8
}

//使用值接收者实现接口:类型的值和类型的指针,都能够保存到接口变量中
/*func (p person) move() {
	fmt.Printf("%s再跑...\n", p.name)
}*/
//使用指针接收者实现接口:只有类型指针能够保存到接口变量中
func (p *person)move()  {
	fmt.Printf("%s再跑...\n", p.name)
}

func main() {
	var m mover
	p1 := person{ //p1是值类型
		name: "雄安王子",
		age:  30,
	}
	p2 := &person{  //p2是person类型的指针
		name: "娜扎",
		age:  18,
	}
	m = p1  //??使用指针接收者实现接口:类型的值,不能赋值给(实现)接口
	m = p2
	m.move()
	fmt.Println(m)
}

1.3 类型和接口的关系

同一个类型可以实现多个接口,不同多个类型也可以实现同一个接口

package main

import "fmt"

//接口的嵌套
type animal interface {
	mover
	sayer
}

//定义接口
type mover interface {
	move()
}
type sayer interface {
	say()
}

//定义结构体
type person struct {
	name string
	age  int8
}

//实现接口和接口方法
//使用值接收者实现接口:类型的值和类型的指针,都能够保存到接口变量中
/*func (p person) move() {
	fmt.Printf("%s再跑...\n", p.name)
}*/
//使用指针接收者实现接口:只有类型指针能够保存到接口变量中
func (p *person) move() {
	fmt.Printf("%s再跑...\n", p.name)
}
func (p *person) say() {
	fmt.Printf("%s再叫···\n", p.name)

}

func main() {
	//	使用接口,定义一个mover类型的变量
	var m mover
	/*	p1 := person{ //p1是值类型
		name: "雄安王子",
		age:  30,
	}*/
	p2 := &person{ //p2是person类型的指针
		name: "娜扎",
		age:  18,
	}
	//m = p1  //??使用指针接收者实现接口:类型的值,不能赋值给接口
	m = p2
	m.move()
	fmt.Println(m)

	//	使用接口,定义一个sayer类型的变量
	var s sayer
	s = p2
	s.say()
	fmt.Println(s)

	var a animal
	a = p2
	a.move()
	a.say()
	fmt.Println(a)
}

1.4 空接口

  1. 接口中没有定义任何需要实现的方法时,该接口就是一个空接口
  2. 任意类型都实现了空接口–> 空接口变量可以存储任意值
  3. 空接口一般不需要提前定义
package main

import "fmt"

//空接口
//接口中没有定义任何需要实现的方法时,该接口就是一个空接口
//任意类型都实现了空接口--> 空接口变量可以存储任意值
//空接口一般不需要提前定义
type xxx interface {
}

//空接口的应用
//1. 空接口类型作为i函数的参数
//2. 空接口可以作为map的value

func main() {
	var x interface{} //定义一个空接口变量x
	x = "hello"
	x = 100
	x = false
	fmt.Println(x)

	//value  是不固定类型,这里我们使用空接口
	var m = make(map[string]interface{}, 16)
	m["name"] = "娜扎"
	m["age"] = 18
	m["hobby"] = []string{"篮球", "足球", "双色球"}
	fmt.Println(m)

}

1.5 断言

package main

import "fmt"

func main() {
	var x interface{}
	x = 100
	x = false
	x = "hello"
	ret, ok := x.(string)
	if !ok {
		fmt.Println("猜错了")
	} else {
		fmt.Println("是字符串", ret)
	}
	
	//使用switch断言
	switch v := x.(type) {
	case string:
		fmt.Printf("是字符串类型,value:%v",v)
	case bool:
		fmt.Printf("是布尔类型,value:%v",v)
	case int:
		fmt.Printf("是int类型,value:%v",v)
	default:
		fmt.Printf("猜不到了,value:%v",v)
	}
}

十三、go基础之反射

package main

import (
	"fmt"
	"reflect"
)

func reflectType(x interface{}) {
	//我不知道别人调用这个函数的时候会传入什么类型的变量
	//  1. 方式1:通过类型断言
	//  2. 方式2:借助反射
	obj := reflect.TypeOf(x)
	fmt.Println(obj)
	fmt.Printf("%T\n", obj)
}

func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	fmt.Printf("%v,%T\n", v, v)
	//如何得到一个v换入时候类型的变量
	k := v.Kind()
	fmt.Println(k)
	switch k {
	case reflect.Float32:
		//把反射取到的值切换成一个float32类型的变量
		ret := float32(v.Float())
		fmt.Println(ret)
	case reflect.Int32:
		ret := int32(v.Int())
		fmt.Printf("%v %T\n", ret, ret)
	}
}
func reflectSetValue(x interface{}) {
	v := reflect.ValueOf(x)
	//Elem()用来根据指针取对应的值
	k := v.Elem().Kind()
	switch k {
	case reflect.Int32:
		v.Elem().SetInt(100)
	case reflect.Float32:
		v.Elem().SetFloat(3.21)

	}

}

type Cat struct {
}

type Dog struct {
}

func main() {
	/*	var a float32 = 1.234
		reflectType(a)
		var b int8 = 10
		reflectType(b)
		var c Cat
		reflectType(c)
		var d Dog
		reflectType(d)*/

	/*var aa int32 = 100
	reflectValue(aa)*/

	var aaa int32 = 10
	reflectSetValue(&aaa)
	fmt.Println(aaa)
}

casbin

持续更新,第二遍学习中···

你可能感兴趣的:(后端冲鸭,计算机各大技术栈学习笔记,golang,学习,开发语言)