[内容][111]
<span id="jump">Hello World</span>
Part 1 开发环境
环境变量设置
GOROOT
指定 golang sdk 的安装目录
GOPATH
golang 工作目录,项目的源码放在这个目录下
PATH
将 GOROOT/bin 放在 Path 路径下,方便命令行能直接运行 golang的命令行工具
Go项目目录结构
|--project // 位于GOPATH下
|--src // 存放源代码
|--packageA
|--packageA.go
|--packageB
|--packageB.go
|--pkg // 编译后生成的文件
|--bin // 编译后生成的可执行文件
Part 2 基础知识
Go的注释
- 行注释
// 行注释
- 块注释(多行注释)
//块注释不可以嵌套
/*
Comment 1
Comment 2
Comment 3
...
Comment n
*/
Go的数据类型
基本数据类型
-
数值型
-
整数型
int8,int16,int32,int64
uint8,uint16,int32,int64
--------------------------------------------------------------------------
byte:uint8的别名
rune:int32的别名
int:64位操作系统则int64,32位操作系统则int32
uint:64位操作系统则uint64,32位操作系统则uint32
-
浮点型
- float32,float64
-
复数型
- complex64,complex128
-
-
布尔型
- bool:占用1个字节,只能接受true和false
-
字符串
- string
复杂数据类型
- 数组 - 值类型
- 结构体 - 值类型
- 指针 - 引用类型
- 管道 - 引用类型
- 函数 - 引用类型
- 接口 - 引用类型
- 切片 - 引用类型
- 映射 - 引用类型
给类型起别名
type myInt int // 此时 myInt 可以作为 int 使用
- 虽然说是起别名,但是 编译器认为 myInt和int不是同一种类型,它们仅仅是底层数据结构相同
Go的变量
变量创建的方法
// 方式1:指定变量类型
var i int // 声明变量 // 可以声明的同时初始化
i = 10 // 为变量赋值 // 等价于 var i int = 10
fmt.Println(i) // 使用变量
// 方式2:使用类型推导 —— 必须显式初始化
var i // 声明变量 // 可以声明的同时初始化
i = 10 // 为变量赋值 // 等价于 var i = 10
fmt.Println(i) // 使用变量
// 方式3:使用简洁语法 —— 不可以用于函数外的变量声明,只能用于局部变量
i := 10 // 声明变量并由类型推导初始化
fmt.println(i) // 使用变量
// 方式4:同时创建多个全局变量 —— 只能用于函数外的变量声明
var (
n1 = 100
name = "gothicrush"
n3 = 200
)
引用类型变量相关
对于引用类型变量,声明后还不能使用,在为它分配内存空间后才能使用
-
分配内存空间有两个函数
-
new(type)
- 只接收一个参数,该参数为一个类型
- 函数返回只想该类型内存地址的指针,该内存地址的值为类型零值
-
make(type, len, prelen)
只适用于 slice, map, channel
-
prelen为预留长度,切片的预留长度需要重新切片后才能使用
a := make([]int, 10, 20) b := a[:cap(a)]
-
变量作用域
- 全局作用域:函数外定义的变量,可以在包中或整个程序(大写开头)中使用
- 局部作用域:函数内定义的变量,只能在函数内使用
- 当函数内部的局部变量和全局变量重名时,编译器采用就近原则,即局部变量覆盖全局变量
变量的注意事项
-
变量声明后,系统自动为它初始化为默认值
在go 中,数据类型都有一个默认值,当程序员没有赋值时,编译器默认给它赋一个默认值
整数型 0 浮点型 0.0 复数型 0+0j 布尔型 false 字符型 "" 复合类型 nil
-
支持一行语句中声明多个变量
var n1 int, n2 string, n3 int //不推荐使用 var n1, name, n3 = 100, "gothicrush", 200 //不推荐使用 n1, name, n3 := 100, "gothicrush", 200 //不推荐使用
-
查看变量的类型和占用字节大小
fmt.Printf("%T", n1) fmt.Printf("%d", unsafe.Sizeof(n1))
-
golang的整型默认为int类型
- 64位操作系统则int64
- 32位操作系统则int32
浮点数都是有符号的,golang的浮点型默认为float64
-
浮点数表示方式
var n1 float64 = 0.12 var n2 float64 = .12 var n3 float64 = 5.123e2
-
golang的字符使用utf-8编码,避免乱码的问题
- 字母:1个字节
- 汉字:3个字节
-
golang中没有专门的字符类型
- 对于ascii编码,用byte存储
- 对于utf-8编码,用rune存储
字符常量用单引号括起来,字符类型可以参与运算,因为它本质上是整型
-
在Go中字符串是不可变的,字符串有两种形式
// 1. 双引号,会识别转义字符 // 2. 反引号,原生输出,原来是什么,就是什么;反引号可以多行
-
字符串拼接
var str = "hello" + " world" + //如果是多行,需要将 + 放在上面 "多行"
Go类型转换
- 基本概念
go中不存在自动(隐式)转换,只有显式转换,就算是低精度->高精度都要显式转换
语法:T(value)
var i int = 42
var b float64 = float64(i)
- 注意事项
- 转换后返回一个新的,被转换的值本身没有变化
- 如果转换过程中发生溢出(比如int64->int8),则不会报错,只是会按溢出处理
基本数据类型与string转换
使用 strconv 包
-
基本数据类型转为string
strconv.Itoa(i)
-
string转为基本数据类型
b, err := strconv.ParseBool(str) i, err := strconv.ParseInt(str, 10, 64) // 10进制,int64 f, err := strconv.ParseFloat(str, 64) // float64 如果转换失败,则返回为默认值,还有err
Go标识符
- 规则
只能由 数字,英文字母和_组成
不能以数字开头
单独的 _ ,是一个特殊符号,不能用作标识符
var _ int = 64 //error
不能用保留关键字,int,float64 等等不是保留关键字,但不推荐使用
-
命名规范
要有意义,要尽量简短 包名:保持package的名字和目录的名字相同 变量名,函数名,常量名:采用驼峰命名法
首字母大写是公有,首字母小写是私有
预定义标识符和保留关键字
- 程序声明:import、package
- 程序实体声明和定义:chan、const、func、interface、map、struct、type、var
- 程序流程控制:go、select、break、case、continue、default、defer、else、fallthrough、for、goto、if、range、return、switch
Part 3 - 指针
获取变量的地址(指针),用 & 操作符,比如 &number
指针类型,存储的是一个地址,比如 *int,*float64
访问指针类型指向空间,用 *,比如 *ptr
指针空值类型是nil ,而不是 null
不支持 ->,一律用 .
不支持指针运算
Part 4 - 包管理
包的概述
- Go中每一个文件都属于一个包,即Go是以包的形式来管理文件和项目目录结构的
- 包的名字规范是全小写
包的作用
- 区分相同名字的标识符
- 控制函数,变量的作用域
- 对函数以及变量进行管理
- 声明某个go文件是某个包
声明包
package db
导入包
-
从 src 目录起,写绝对路径
// 单个导入 import "go_code/chapter03/demo04/model" // 批量导入 import ( "go_code/chapter03/demo04/model_1" "go_code/chapter03/demo04/model_2" "go_code/chapter03/demo04/model_3" ) // 如果包太长,可以给包改名,也可以避免重名的问题 import model "go_code/chapter03/demo04/model"
-
导入包可以写绝对路径和相对路径
- 绝对路径:从 src 目录起,写绝对路径
- 相对路径:以当前文件为基准点
包的搜索路径
- 标准库
- GOPATH指定目录/src
包的init函数
- 一个包一旦被导入,就会自动执行这个包中的init函数
- 一个包中可以有多个init函数
- 可以配合 _ 来使用
包的注意事项
文件名和包名最好相同
-
如果导入了包而不使用则会编译错误,如果只想执行包中的init函数而不想使用,则可以用 _
import _ "packagename"
-
如果想要定义一个go文件是可执行文件,则需要将这个文件所属包声明为main
package main // 这是规范
不能将多个包放在同一目录下,也不能将一个包拆分到多个目录下,总之一个包就是一个目录,这个目录名和包名相同
对于main包中不同的文件的代码不能互相调用
Part 5 - 运算符
运算符分类
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- 位运算符
- 其他运算符
算术运算符
- 对数值类型使用
- +,-,*,/,%,++,--
- / :如果两边都是整数,则结果会把小数直接舍去
- % :公式: a % b = a - a / b * b
- ++/-- :只能作为独立的语句使用,即单独占一行,++和--只能写在变量的后面
比较运算符
- 用在条件判断或循环判断中,返回值是true或false
- ==, >, < , >=, <=, !=
逻辑运算符
- 用在条件判断或循环判断中,参与运算的是true或false,返回值是true或false
- &&(短路), ||(短路), !
赋值运算符
=, +=, -=, *=, /=, %=
<<=, >>=, &=, |=, ^=
赋值运算左边只能是变量,右边可以是变量,表达式,常量值
-
交换变量而不使用中间变量
var a int = 10 var b int = 20 a = a + b b = a - b // b = (a+b) - b = a a = a - b // a = (a+b) - a = b
其他运算符
- Go官方明确声明不支持 三元运算符
- 取地址 &
- 解指针 *
Part 6 - IO
从键盘获取数据
import fmt
// fmt.Scanln() // 换行时停止扫描
var name string
var age byte
var sal float32
var isPass bool
fmt.Scanln(&name)
fmt.Scanln(&age)
fmt.Scanln(&sal)
fmt.Scanln(&isPass)
// fmt.Scanf() // 扫描文本,根据format参数指定格式将读取空白分隔的值保存进本函数中
var name string
var age byte
var sal float32
var isPass bool
fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)
读取命令行参数
-
方式一:通过 os.Args
func main() { for index,val := range os.Args { fmt.Println(index, val) } }
-
方式二:通过 flag 包
import "flag" func main() { var user string var pwd string var host string var port int flag.StringVar(&user, "u", "","用户名,默认为空") flag.StringVar(&pwd, "pwd", "","密码,默认为空") flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost") flag.StringVar(&port, "p", "3306", "端口号,默认为3306") flag.Parse() }
文件的输入流与输出流
什么是输入流/输出流
文件在程序中是以流的形式进行操作的
输入流:数据从文件到内存
输出流:数据从内存到文件
文件操作的包
- Go中文件操作的API都在 os 库中
打开文件
func os.Open(path string) (f *File, err error)
关闭文件
func (f *File) Close() error
配合 defer 语句使用
读文件
- 带缓存读
// 默认缓冲区大小为4096
reader := bufio.NewReader(file)
for {
str,err := reader.ReadString('\n')
if err == io.EOF {
break
}
}
- 一次性读取
func ioutil.ReadFile(path string) (content []byte, err error) {}
file := "/home/test.text"
content, err := ioutil.ReadFile(file) // 不需要打开和关闭文件,这些操作都封装到ReadFile函数中了,适合小文件读取
if err != nil {
fmt.Println(string(content))
}
写文件
func OpenFile(path string, flag int, perm FileMode) (file *File,err error)
- 模式 flag,可以通过 | 组合使用
O_RDONLY
O_WRONLY
O_RDWR
O_APPEND
O_CREATE
O_EXCL
O_SYNC
O_TRUNC
- 权限 perm
r -> 4
w -> 2
x -> 1
-
案例
-
创建一个新文件,写入5句 "Hello World"
file := "/home/abc.txt" file, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE, 0666) if err != nil { fmt.Println("open file successfully") return } defer file.Close() str := "hello, world" writer := bufio.NewWriter(file) for i := 0; i < 5; i++ { writer.WriterString(str) } // 因为带缓存,因此要刷新缓存 writer.Flush()
-
打开一个存在的文件,将原来的内容覆盖为新的内容,10句 "你好"
file := "/home/abc.txt" file, err := os.OpenFile(file, os.O_WRONLY | os.O_TRUNC, 0666) if err != nil { fmt.Println("open file successfully") return } defer file.Close() str := "你好你好" writer := bufio.NewWriter(file) for i := 0; i < 10; i++ { writer.WriterString(str) } // 因为带缓存,因此要刷新缓存 writer.Flush()
-
打开一个文件,在原来的内容基础上追加内容 " Append"
file := "/home/abc.txt" file, err := os.OpenFile(file, os.O_APPEND | os.O_TRUNC, 0666) if err != nil { fmt.Println("open file successfully") return } defer file.Close() str := " APPEND" writer := bufio.NewWriter(file) for i := 0; i < 10; i++ { writer.WriterString(str) } // 因为带缓存,因此要刷新缓存 writer.Flush()
-
打开一个存在的文件,将原来的内容读出显示在终端,并追加5句 "你好,世界"
file := "/home/abc.txt" file, err := os.OpenFile(file, os.O_RDWR | os.O_APPEND, 0666) if err != nil { fmt.Println("open file successfully") return } defer file.Close() reader := bufio.NewReader(file) for { str, err := reader.ReadString("\n") if err == io.EOF { break } fmt.Println(str) } str := "你好,世界" writer := bufio.NewWriter(file) for i := 0; i < 5 i++ { writer.WriterString(str) } // 因为带缓存,因此要刷新缓存 writer.Flush()
-
将一个文件的内容,写入到另外一个文件(注意:这两个文件已经存在了)
file1Path := "/home/abc.txt" file2Path := "/home/def.txt" data, err := ioutil.ReadFile(filePath) if err != nil { fmt.Println("文件读取失败") return } err = ioutil.Write(file2Path, data, 0666) if err != nil { fmt.Println(err) }
-
判断文件与目录是否存在
根据 os.Stat() 函数返回值进行判断
1. 如果返回值为nil,说明文件或目录存在
2. 如果返回值使用os.IsNotExist()判断为true,则说明文件或目录不存在
3. 如果返回值为其他类型,则不确定是否存在
func PathExisting(path string) (bool,error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
拷贝文件
func io.Copy(dst Writer, src Reader) (Written int64, err error)
func CopyFile(dstFilePath string, srcFilePath) (written int64, err error) {
srcFile, err := os.Open(srcFilePath)
if err != nil {
fmt.Println("读取源文件错误")
return
}
defer srcFile.Close()
reader := bufio.NewReader(srcFile)
dstFile, err := os.OpenFile(dstFilePath, os.O_WRONLY | os.O_CREATE, 0666)
if err != nil {
fmt.Println("打开文件失败")
}
defer dstFile.Close()
writer := bufio.NewWriter(dstFile)
return io.Copy(writer, reader)
}
Part 7 - 流程控制
分支流程控制
-
单分支
if 条件表达式 { // { } 必须要有,且左大括号不能换行 // }
-
双分支
if 条件表达式 { // { } 必须要有,且左大括号不能换行 } else { // { } 必须要有,且左大括号不能换行,else必须和右大括号同行 }
-
多分支
if 条件1 { // { } 必须要有,且左大括号不能换行 } else if 条件2 { // { } 必须要有,且左大括号不能换行,else必须和右大括号同行 } else if 条件n { // { } 必须要有,且左大括号不能换行,else必须和右大括号同行 } else { // { } 必须要有,且左大括号不能换行,else必须和右大括号同行 // 多分支中 else 不是必须的 }
-
if 语句可以初始化变量
if ok := function(); ok { }
-
switch语句
switch 表达式 { case 表达式1,表达式2,...: 语句1 case 表达式3: 语句2 default: 语句 } // case的表达式要求不能重复,case表达式值与switch表示值需要是同一类型 // case后面可以跟多个表达式,用逗号分隔 // 一旦匹配,就不再进行下一步匹配 // default不是必须的,可有可无 // 如果匹配不到,则执行default块(如果有) // 匹配选项后面不用加 break,默认不贯穿 // 如果想要穿透,则用 fallthrough 声明 // switch后也可以不跟表达式,效果等同 switch True {} // switch后可以先声明一个变量再使用 switch i:=100 {}
-
if-else if-else 与 switch 使用场景对比
// 对于匹配区间 推荐用 if-else if-else // 对于匹配具体值 推荐用 switch
循环分支流程控制
Go中只有 for 没有 while 也没有 do-while
-
for 使用方法-1 普通 for 循环
for 循环变量初始化; 循环条件; 循环条件迭代 { // 循环操作语句 }
-
for 使用方法-2 模拟 while 循环
for 循环条件 { // 循环操作语句 }
-
for 使用方法-3 死循环
for { // 死循环 } // 等价于 for ;; { // 死循环 }
-
for 使用方法-4 for-range
for index,val := range str { fmt.Println(index, val) }
-
for使用方法-5 新形式
for 初始化; 循环条件 { // 循环操作语句 }
-
for 注意事项
循环条件返回的 bool 的表达式
-
break 可以根据标签跳出
break //跳出最近的一层 break label1 //跳到label1指定位置 break label2 //跳到label2指定位置 for i := 0; i < 4; i++ { label1: for j :=0; j < 10; j++ { if j == 2 { break label1 } fmt.Println("j =", ) } }
-
continue 可以根据标签跳出
continue //跳出最近的一层 continuecontinue label1 //跳到label1指定位置 break label2 //跳到label2指定位置 for i := 0; i < 4; i++ { label1: for j :=0; j < 10; j++ { if j == 2 { continue label1 } fmt.Println("j =", ) } }
goto语句
不推荐使用 goto 语句
-
语法
label: 语句 goto label
Part 8 - 函数
定义函数
func 函数名 (形参列表) (返回值列表) { // 左大括号不能换行
//执行语句
return xxx //可有可无
}
函数参数简化
func test(a int, b int, c int) {}
等价于
func test(a,b,c int) {}
函数返回值
Go语言可以返回多个值,如果返回不想接收,可以用 _ 来省略
如果只返回一个值,则返回值列表可以不加()
-
不指定返回值名称
-
指定返回值名称
func add(a int, b int) (result int) { result = a + b return }
可变参数
- 会把不定参数封装为一个slice
- 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
func sum (args... int) total int {
for _, val := range args {
total += val
}
return
}
sum(1,2,3,4,5,6,7)
函数注意事项
基本数据类型和数组默认是值传递的,即进行值拷贝,在函数内部修改,不会影响到原来的值
如果希望函数内部修改可以影响到外面,则需要使用指针
Go函数不支持函数重载,不支持嵌套,不支持默认参数
-
Go函数本身也是一种数据类型,可以将函数作为参数传递
func getSum(n1 int,n2 int) int { return n1 + n2 } func myFunc(funvar func(int,int) int, num int, num2 int) int { return funvar(num1, num2) }
-
为函数类型起别名
type myf func(int,int) int
init函数
名字固定,每一个Go源文件中都可以包含这样的一个函数,该函数在main函数执行前,会先被调用
-
该函数可以用作初始化操作
func init() { // 无形参,无返回值 }
-
如果有全局变量定义,则执行顺序为
全局变量定义 -> init函数 -> main函数
匿名函数
-
只调用一次的匿名函数
res := func (n1 int, n2 int) int { return n1 + n2 }(1,2)
-
可调用多次的匿名函数
a := func (n1 int, n2 int) int { return n1 + n2 } a(1,2) a(3,4) a(5,6)
-
全局匿名函数
var ( gFun = func(n1 int, n2 int) int { return n1 + n2 } )
闭包
闭包就是 一个函数A中定义另外一个函数B,且函数B中有使用到函数A中的局部变量,且函数A将函数B返回
defer
-
作用
defer是函数的延时执行机制 defer语句当函数结束时才调用 defer语句一般用于释放资源
-
注意
多个defer语句会放到一个栈中 即最先声明的defer语句会在最后执行 当语句入栈时,也会将相关的值进行拷贝
函数参数传递方式
- Go语言一种有两大类数据类型:值类型 ;引用类型
- Go中值类型包括:int系列,float系列,complex系列,bool,string,数组和结构体
- Go中引用类型包括:slice,map,chan,interface,指针
- 值类型的默认传递方式是值传递,引用类型的默认传递方式是引用传递
内置函数
len:用来求长度,如string,array,slice,map,channel
-
new:用来分配内存,主要用于分配值类型
numPtr := new(int) 这个numPtr是 *int 类型
make:用来分配内存,主要用于分配引用类型
cap:求切片的容量
Part 9 - 字符串常用操作
len(str):返回字符串的长度(按字节,字母1个字节,汉字3个字节)
-
遍历字符串
r := []rune(str) for i := 0; i < len(r); i++ { fmt.Printf("%c\n",r[i]) }
-
字符串转整数
import strconv n, err := strconv.Atoi("123")
-
整数转字符串
import strconv n := strconv.Itoa(123)
-
字符串转[]byte
var n []byte = []byte("hello")
-
[]byte转字符串
str := string([]byte("123456"))
-
10进制转2,4,8,16进制
str := strconv.FormatInt(132,2) str := strconv.FormatInt(132,4) str := strconv.FormatInt(132,5) str := strconv.FormatInt(132,16)
-
查找字符串中有无特定子串
import strings b := strings.Contains("seafood", "food")
-
统计字符串中有几个特定子串
import strings c := strings.Count("ceeesseeee","e")
-
查找子串在字符串从左到右第一次出现的下标值,没有则返回-1
index := strings.Index("NTL_abc", "abc")
-
查找子串在字符串从右到左第一次出现的下标值,没有则返回-1
index := strings.LastIndex("NTL_abc", "abc")
-
如果字符串中有特定子串,则将其换为其他子串
str := strings.Replace("go hello", "go", "python")
-
字符串按某个子串进行分隔,返回slice
sl := strings.Split("hello-world-ok","-")
-
将字符串进行大小写转换
str := string.ToLower("HELLO") str := string.ToHigher("hello")
-
字符串去空格
去除左右两边的空格,strings.TrimSpace(" 1 2 ") 去除左右两边特定子串,strings.Trim("! 1 2 !", "!") 去除左边特定子串,strings.TrimLeft("! 1 2 !", "!") 去除右边特定子串,strings.TrimRight("! 1 2 !", "!")
-
判断字符串是否以某个子串开头/结尾
strings.HasPrefix("NLT_abc","NLT") strings.HasSuffix("NLT_abc","abc")
Part 10 - 时间和日期常用操作
-
相关包
import time
-
获取当前时间
now := time.Now()
-
获取当前年月日,时分秒
now := time.Now() now.Year() now.Month() now.Day() now.Hour() now.Minute() now.Second()
-
日期时间格式化
// 方法1 "%d/%d/%d %d:%d:%d",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second() // 方法2 now.Format("2006/01/02 15:04:05") "2006/01/02 15:04:05" 这个时间固定的,必须这么写 "2006/01/02 15:04:05" 中各个数字可以自由组合,这样可以格式化输出时间
-
时间常量
时 time.Hour 分 time.Minute 秒 time.Second 毫秒 time.Millisecond 微妙 time.Microsecond 纳秒 time.Nanosecond
-
休眠
time.sleep(10 * time.Millisecond)
-
获取当前时间戳
time.Now().Unix() // 从1970年到现在所经过的时间(单位秒),类型int64 time.Now().UnxiNano() // 从1970年到现在所经过的时间(单位纳秒),类型int64
Part 11 - 错误处理机制
panic-defer-recover机制
- 默认情况下,当发生错误(panic)后,程序就会崩溃
- 如果希望当发生 panic 后,可以捕捉到 panic,并进行处理,使程序不崩溃,则需要进行错误处理
- Go语法优雅,不支持传统的 try...catch....finally,而是使用 defer, panic, recover机制
- 流程:抛出一个panic,然后在 defer 语句中通过 recover 函数捕获这个 panic,并处理
func main() {
test()
fmt.Println("尽管发生了 panic,程序还是继续执行了")
}
func test() {
defer func() {
err := recover() // recover内置函数,可以捕获异常
if err != nil {
fmt.Println("err=",err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}
自定义异常
-
使用errors.New和panic内置函数
- error.New("错误说明"),返回一个 error 类型的值,表示一个错误
- panic(),内置函数,接收任意值作为参数,引发异常
import "errors" func readConf(name string) (err error) { if name == "config.ini" { return nil } else { return errors.New("读取配置文件错误") } } func test() { err := readConf("config.ini") if err != nil { panic(err) } }
注意事项
- panic可以在任何位置引发,但是 recover 只能在 defer 中调用
- panic("message") 指定 panic 发生时显示的信息
Part 12 - 数组
什么是数组
- 数组是一种数据类型,属于值类型
- 数组可以存放多个同一类型数据
数组定义
var 数组名 [数组大小]数据类型
var a [5]int
数组初始化的4种方式
var numArray01 [3]int = [3]int{1,2,3}
var numArray02 = [3]int{1,2,3}
var numArray03 = [...]int{1,2,3} // 长度自行推导
var numArray04 = [3]{0:1,2:3} // 指定下标
数组遍历
// 方法1:
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
// 方法2:
for index, value := range arr {
fmt.Println(index, " ", value)
}
数组对比
- 数组可以用 == 和 != 进行比较
- 但不能用 > < >= <= 等符号
数组相互赋值
- 当数据类型和数组长度相同时,两个数组之间可以相互赋值
数组注意事项
数组一旦定义后,长度不能改变,且数组中每个元素都有数据类型对应的默认值
可以通过数组名来获取数组第一个元素的地址,即 数组名 == &数组名[0] == 数组首地址
数组中各个元素的地址间隔由数组类型决定,比如 int64 -> 相隔8,int32 -> 相隔4
数组是值类型,作为参数时,是值传递,即在函数内修改数组,不会对原数组影响,除非使用指针
不能将 [3]int 类型的数组传递给 [4]int 的参数,它们被认为是不同类型;但是 [...]int{1,2,3} 可以传递给 [3]int 的参数
-
对于指向数组的指针,仍然可以用[index]进行索值
var arr [5]int{1,2,3,4,5} var p = &arr // p[3] == arr[3]
二维数组
-
声明
var 数组名 [大小][大小]类型
-
赋值
数组名[n][m] = 123 没有赋值或初始化就是默认值
-
初始化
var 数组名 [大小][大小]类型 = [][大小][大小]类型{{初值},{初值},{初值}} var 数组名 [大小][大小]类型 = {{初值},{初值},{初值}} var 数组名 = [大小][大小]类型{{初值},{初值},{初值}} var 数组名 = [...][大小]类型{{初值},{初值},{初值}}
-
使用
fmr.Println(数组名[n][m])
-
遍历
// 方法1: for i := 0; i< len(arr); i++ { for j := 0; j < len(arr[i]); j++ { fmt.Println(arr[i][j]) } } // 方法2: for i,v := range arr { for j,v2 := range v { fmt.Println(v2) } }
示意图
Part 13 - 切片
什么是切片
- 切片是引用类型,指向一个结构体,结构体包括一个数组的指针,切片大小,切片容量
- 切片的长度是可变的,因为切片底层是动态数组,所以切片的操作和数组很类似
定义切片
var 切片名 []类型
var s []int
切片初始化
// 如果没有给切片赋值,则是类型的默认值
// 如果切片没有初始化,也是可以使用的,这与 map 必须初始化后才能使用不同
// 方式1:直接初始化
var s []int = []int{1,3,5}
// 方式2:由已存在数组创建
var intArr [5]int = [...]int{1,2,3,4,5}
s := intArr[1:3] // [1,3)
// arr[0:end] 等价 arr[:end]
// arr[start:len(str)] 等价 arr[start:]
// arr[0:len(str)] 等价 arr[:]
// 方式3:通过 make 来创建切片
// 通过 make 方法创建的切片,其底层数组是 make 内部维护的,外部不可见
// 所以切片的值是默认值
var s []int = make([]int, 4) // 只指定了 length,则capacity == length
var s []int = make([]int, 4, 10) // []type, length, capacity
切片遍历
var arr [5]int = [...]int{10,20,30,40,50}
slice := arr[0:3]
// 方式1
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
// 方式2
for i, v := range slice {
fmt.Println(i, v)
}
切片追加元素
var slice []int = []int{1,2,3}
slice = append(slice,5,6,7) // 返回新的 slice
slice = append(slice,slice) // 可以把slice追加给slice
切片append操作原理
- 切片append操作本质就是对数组进行扩容
- Go底层会创建一个新的数组newArr
- 将slice原来的元素拷贝到新的数组newArr中
- newArr是底层维护的,程序员不可见
- 然后创建一个新的sliceNew,sliceNew的ptr指向newArr
- 最后返回 sliceNew
- 当切片容量少于等于1000时,以2倍扩容,当大于1000时,以1.25倍扩容
切片拷贝操作
copy(para1,para2) //para1 和 para2 都是切片类型,将para2的内容复制到para1
切片内存布局
切片是引用类型,切片名变量存储的是一个结构体的地址,即切片名是结构体的指针
-
结构体包含三个值:
封装数组的地址
切片的大小
-
切片的容量
type slice struct { ptr *[2]int length int capacity int }
切片和字符串
-
字符串底层是 []byte,因此也可以切片
str := "hello world" slice := str[0:5]
-
字符串是不可变的
str[0] = 'z' // error // 正确 var temp []byte = []byte(str) // 只能处理字母和数字,因为中文是3个字节 temp[0] = 'z' str = string(temp) // 如果想要处理中文,则转为rune即可 var temp []rune = []rune(str) temp[0] = 'z' str = string(temp)
基于原有切片定义新切片
slice := []int{1, 2, 3, 4, 5}
slice1 := slice[:]
slice2 := slice[0:]
slice3 := slice[:5]
- 设置切片长度和容量一样的好处
让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片
空切片和nil切片
nil切片: var slice []int
空切片: slice := make([]int,0)
Part 14 - 结构体
定义结构体
type Cat struct {
Name string
Age int
Color string
Hobby []string
}
结构体实例化的4种方法
// 方法1
var catInstance Cat // 该变量中的字段的值都是默认类型
// 方法2
var catInstance Cat = Cat{"小白", 2, "white", []string{"吃鱼","喝奶"}}
var catInstance Cat = Cat {
Name: "小白",
Age: 2,
Color: "white",
Hobby: []string{"吃鱼","喝奶"},
}
// 方法3
var catInstancePtr *Cat = new(Cat)
(*catInstancePtr).Name = "小白" //等价于 catInstancePtr.Name = "小白"
// 方法4
var catInstancePtr *Cat = &Cat{}
(*catInstancePtr).Name = "小白" //等价于 catInstancePtr.Name = "小白"
// 不能写 *catInstancePtr.Name = "小白" 因为 . 的优先级高于 *
结构体实例初始化问题
// 如果没有给字段赋值,则默认为零值
// 引用类型是nil,即还没有分配空间,对于这样的字段,需要先make,才能使用
var cat1 Cat
fmt.Println(cat1.Name, cat1.Age, cat1.Color) //ok
fmt.Println(cat1.Hobby) //error,需要初始化后才能使用
结构体与tag
-
在定义结构体时,可以给每个字段加上一个tag
type Student struct { Name string "tag-name" Age int "tag-age" }
结构体的tag可以通过反射机制获取,常见用于序列化和反序列化
匿名结构
name := struct {
字段名1 类型
字段名2 类型
字段名3 类型
} {
字段名1:value,
字段名2:value,
字段名3:value
}
//注意匿名的东西都只能使用":="的形式,因为无法指定类型
匿名字段
type Person struct {
string
int
}
a := Person{"narlinen",20}
工厂模式
-
结构体没有构造函数,通过工厂模式来解决
type student struct { Name string Age int } func New(name string, age int) *student { return &student { Name: name, Age: age, } }
结构体注意事项
- 结构体的所有字段在内存中是连续的
- 结构体没有构造函数,通过工厂模式创建实例
- 结构体是用户单独定义的类型,和其他类型进行转换时,需要具有完全相同的字段(名字以及数量,以及类型)
- 用type给已定义的结构体起别名时,编译器会认为这是新的数据类型,它们之间的赋值需要显式转换
- 就算字段内容完全相同,但只要struct名字不一样,则就不相等
- 即 type Duration int64 后,Duration和int64仍然是不同类型,需要强制转换
Part 15 - 方法
什么是方法
- 方法类似于函数,只不过方法可以进行绑定,方法可以绑定到指定的数据类型上,使该数据类型的实例都可以使用这个方法
- 方法不能独自调用,只能通过绑定的数据类型的实例进行调用
- 自定义类型都可以有方法,不仅仅是struct
方法的定义与使用
type A struct {
Name string
}
func (a A) test() {
fmt.Println(a.Name)
}
func main() {
a := A{Name:"111"}
a.test()
}
可以添加方法的类型
- 任何的自定义类型都可以添加相应的方法
方法注意事项
接收者必须有一个显式的名字,这个名字必须在方法中被使用
-
如果方法不需要使用接收者的值,可以用 _ 替换它
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法
-
Go中的toString()方法
func (a type) String() string { ... }
值接收者和指针接收这
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法,接收者为值类型时,该类型的值类型或指针类型都可以直接调用,反之亦然
- 接收者为值类型
- 调用者为对应值类型:直接调用
- 调用者为对应类型的指针类型:编译器底层自动解指针后再调用
- 接收者为指针类型
- 调用者为对应值类型:编译器底层自动取地址后再调用
- 调用者为对应值类型的指针类型:直接调用
- 无论调用者是值类型还是指针类型,实际调用类型还是方法定义的接收者类型
- 接收者为值类型
Part 16 - 封装
- Go中字段访问权限分为两种:私有字段和公有字段
- 这两种权限仅仅针对包级别有用
- 公有字段:首字母大写
- 私有字段:首字母小写
Part 17 - 继承
继承的实现方法
如果一个 struct 嵌套了另外一个匿名结构体,那么这个结构体就可以直接访问匿名结构体的字段和方法,从而实现继承
继承实现的例子
type Goods struct {
Name String
Price float64
}
type Book struct {
Goods // 这里就是嵌套匿名结构体 Goods
Writer string
}
继承注意事项
-
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或小写的字段,方法都可以使用
type A struct { Name string age int } func (a *A) SayOK() { fmt.Println("A SayOk", a.Name) } func (a *A) hello() { fmt.Println("A Hello", a.Name) } type B struct { A } func main() { var b B; b.A.Name = "tom" b.A.age = 10 b.A.SayOk() b.A.hello() }
-
匿名结构体字段访问可以简化
type A struct { Name string age int } func (a *A) SayOK() { fmt.Println("A SayOk", a.Name) } func (a *A) hello() { fmt.Println("A Hello", a.Name) } type B struct { A } func main() { var b B; b.Name = "tom" b.age = 10 b.SayOk() b.hello() }
-
当结构体和匿名结构体有相同名字的字段或者方法时
- 编译器采用就近访问原则,即默认访问该结构体中的字段/方法
- 编译器会先看结构体中有没有这个字段/方法
- 如果有,则直接使用这个结构体中的字段/方法
- 如果没有,则看嵌套的匿名结构体中有没有这个字段/方法
- 如果有,使用匿名结构体中的字段/方法
- 如果没有,报错
- 如果希望访问匿名结构中的字段或方法,可以通过匿名结构体名字显式调用
type A struct { Name string } type B string { A Name String } func main() { var b B = B{ Name: "I'm B" } b.A.Name = "I'm A" fmt.Println(b.Name) // 显示 I'm B fmt.Println(b.A.Name) // 如果想要使用A的Name,则需要显式说明 }
-
结构体中嵌入两个或以上的匿名结构体,如果两个匿名结构体具有相同名字的字段/方法(同时结构体本身没有这个名字的字段/方法),则在访问时,必须显式指定匿名结构体的名字,否则编译器报错
type A struct { Name string age int } type B struct { Name string Score float64 } type C struct { A B } func main() { var c C c.Name = "tom" // error c.A.Name = "A-tom" // ok c.B.Name = "B-tom" // ok }
type A struct { Name string age int } type B struct { Name string Score float64 } type C struct { A B Name string } func main() { var c C c.Name = "tom" // ok,因为结构体本身具有 Name }
-
嵌套基本类型
type A struct { Name string age int } type Stu struct { A int } func main() { stu := Stu{} stu.Name = "tom" stu.age = 100 stu.int = 666 fmt.Println(stu) }
继承的初始化
type Brand struct {
Name string
Address string
}
type Goods struct {
Name string
Price float64
}
type TV struct {
Goods
Brand
}
func main() {
tv := TV {
Goods{
Name: "电视机",
Price: 15.5
},
Brand{"海尔","上东"},
}
}
使用指针形式的继承
type Brand struct {
Name string
Address string
}
type Goods struct {
Name string
Price float64
}
type TV struct {
*Goods
*Brand
}
func main() {
tv := TV {
&Goods{
Name: "电视机",
Price: 15.5
},
&Brand{"海尔","上东"},
}
}
Part 18 - 组合
组合与继承的关系
组合和继承基本相同
-
区别在于在结构体中嵌套其他结构体时,进行命名,而不是匿名
type Person struct { Name string Age int } type Student struct { person Person Score float64 }
-
对于组合,如果想要使用嵌套结构体中的字段/方法,则必须显式说明嵌套结构体的名字
type Person struct { Name string Age int } type Student struct { person Person Score float64 } func main() { var stu Student =Student{} stu.person.Name = "gothicrush" stu.person.Age = 21 stu.Score = 100.0 }
Part 19 - 接口
接口定义
type usb interface {
Start()
Stop()
}
接口实现
- Go中不需要显式声明继承、实现了接口
- 只要一个类型实现了接口中所有的方法,则编译器就认为该类型实现了接口
type Phone struct {
}
func (p Phone) Start() {
fmt.Println("手机连接成功")
}
func (p Phone) Stop() {
fmt.Println("手机停止连接")
}
接口注意事项
接口中声明一组方法,不需要也不能实现
接口中不能有任何变量或常量,只能有声明的方法
接口类型是引用类型,如果没有对它赋值,则默认为nil
接口本身不能拥有实例,但是接口类型的变量可以指向一个实现了该接口的数据类型变量(即多态)
-
只要是自定义数据类型,都可以实现接口,不仅仅是结构体
type Speak interface { Say() } type integer int func (i integer) Say() { fmt.Println("I'm integer") }
一个自定义类型可以实现多个接口
-
一个接口A可以继承一个或多个其他接口,此时,如果想要实现接口A,则需要把A继承的接口也都全部实现
- 如果多个接口中有方法的名字相同,则会报错
- 就算参数不同,返回值不同也不行,只要名字相同就报错,因为Go不支持函数重载
type BInterface interface { testB() } type CInterface interface { testC() } type AInterface interface { BInterface CInterface testA() } type Student struct { } func (stu *Student) testB() { } func (stu *Student) testC() { } func (stu *Student) testA() { }
- 如果多个接口中有方法的名字相同,则会报错
-
空接口 interface{} 没有任何方法,所以所有类型都实现了空接口
interface{} 这个类型的变量可以指向任何类型的变量 所以 interface{} 类似 Java 的 Obeject
-
当实现方法时,struct 和 *struct 是不同的
type Usb interface { Say() } type Student struct { } func (stu *Student) Say() { fmt.Println("Say..") } func main() { var stu Student = Student{} var u1 Usb = stu // 报错,因为是 *Student 实现了Usb,而不是 Student var u2 Usb = &stu // ok }
接口和继承的关系
- 当结构需要扩展功能,同时不希望去改变或破坏继承关系,则可以去实现某个接口
- 可以认为接口是对继承机制的补充
- 继承:解决代码的复用性和可维护性
- 接口:用于设计和规范代码
Part 20 - 多态
- Go的多态依赖接口实现,因为 Go 中没有像 Java 那样的继承
Part 21 - 类型断言
什么是类型断言
- 由于多态的存在,接口变量不知道其指向的具体类型,如果需要转为具体类型,则需要使用类型断言
类型断言语法
接口变量名.(具体类型) // 此处变量必须为 interface 类型
类型断言返回值
x := 变量名.(具体类型) // 如果转换成功则返回给x,转换失败则抛出 panic
x,ok := 变量名.(具体类型) // 如果转换成功则返回给x,ok为true,转换失败则 ok 为 false,x为类型默认值
类型断言例子
type Point struct {
x int
y int
}
func main() {
var a interface{}
var point = Point{1,2}
a = point
var b Point
// b = a //不行,虽然 a 指向的是Point类型,但是现在 a 是 Point 类型
b = a.(Point) // 可以,这就是类型断言,表示判断 a 是否指向 Point 类型的变量
// 如果是则转为 Point 类型并赋值给 b 变量,否则抛出 panic
}
类型断言的最佳实践
func checkType(items ...interface{}) {
for index, x := range itmes {
switch x.(type) { // 这里 type 是关键字,固定写法,只能用于 switch 语句
case bool:
fmt.Println("bool")
case float64:
fmt.Println("float64")
case string:
fmt.Println("string")
case Student:
fmt.Println("Student")
case *Student:
fmt.Println("*Student")
}
}
}
Part 22 - 单元测试
传统测试方法
- 在 main 函数中,调用需要测试的函数,看看实际结果与预期是否相同,如果相同,则正确,否则不正确
- 缺点:
- 不方便,我们需要在 main 函数中调用,如果项目正在运行,则可能需要去停止项目
- 不利于管理,不管什么模块的方法的测试都写在 main 函数里面了
单元测试框架
Go中自带有一个轻量级测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试
-
使用流程
-
创建测试文件
以 xxx_test.go 格式命名
-
导入测试框架
import "testing"
-
测试函数的签名
// 参数类型必须为 *testing.T(单元测试)或 *testing.B(性能测试) // 必须以Test开头,Test后的下一个字母必须是大写字母,否则单元测试不会执行这个函数 func TestXxx(t *testing.T) { }
-
编写测试函数
func TestXxx(t *testing.T) { t.SkipNow() // 跳过当前测试函数 t.Fatalf("错误") t.Errorf("错误") t.Logf("正确") }
-
执行测试
go test // 所有测试文件都进行测试,如果运行正确,无日志,错误时,有日志 go test -v // 所有测试文件都进行测试,无论运行是否正确,都有日志 go test xxx_test.go // 指定测试文件 go test -v xxx_test.go // 指定测试文件 go test -v -test.run TestAdd // 只测试指定的函数
-
保证多个测试函数按顺序执行
func TestAll(t *testing.T) { // 不一定是叫 TesttAll t.Run("TestPrint",TestPrint) // TestPrint为函数名 t.Run("TestPrint2",TestPrint2) }
-
TestMain
- 固定名字
- func (m testing.M) {}
- 测试函数最开始执行这个函数,一般用于测试环境初始化
- 如果 TestMain 中不写 m.Run(),则其他测试函数不会执行
- 固定名字
-
benchmark
用于性能测试
参数为(b *testing.B),程序会执行 b.N 次,在执行过程中,会根据实际case的执行时间是否稳定改变b.N的值,以达到稳态
-
例子
func BenchmarkTest(b *testing.B) { for n := 0; n < b.N; n++ { function() } }
-
benchmark中的函数必须是稳定的,否则测试程序不能停止
func increse(n int) int { for n > 0 { n-- } return n } func BenchmarkAll(b *testing.B) { for n := 0; n < b.N; n++ { increase(n) // 函数每次执行时间都不同 } }
Part 23 - JSON/序列化/反序列化
JSON概念以及作用
- JSON全程 JavaScript Object Notation,是一种轻量级的数据交换格式,易于阅读和书写,也方便机器进行生成和解析
- 在数据传输前,传输的数据会经过处理变为 JSON 形式的字符串后再通过网络传输,接收方接收到 JSON 字符串后会进行处理,将 JSON字符串转为原始的数据
- 序列化:原始数据 -> JSON字符串
- 反序列化:JSON字符串 -> 原始数据
序列化所需的包以及函数
import "encoding/json"
func Marshal(v interface{}) ([]byte, error)
结构体进行序列化
-
将要进行序列化的结构体
type Monster struct { Name string Age int BirthDay string Sal float64 Skill string }
-
结构体实例化实例
import "encoding/json" func main() { monster := { Name: "牛魔王", Age: 500, Birthday: "2011-11-11", Sal: 8000.0, Skill: "牛魔拳", } data,err := json.Marshal(monster) if err != nil { fmt.Println("序列化失败", err) } else { fmt.Println("序列化后:", string(data)) } }
-
结构体使用tag自定义序列化后 key 值的名称
// `json:"xxx"` // 反引号是必须的 type Monster struct { Name string `json:"name"` Age int `json:"age"` BirthDay string `json:"birthday"` Sal float64 `json:"sal"` Skill string `json:"skill"` }
map进行序列化
-
将要进行序列化的map
var m map[string]interface{} m["name"] = "红孩儿" m["age"] = 30 m["address"] = "洪崖洞"
-
map实例化实例
import "encoding/json" func main() { var m map[string]interface{} m["name"] = "红孩儿" m["age"] = 30 m["address"] = "洪崖洞" data,err := json.Marshal(m) if err != nil { fmt.Println("序列化失败", err) } else { fmt.Println("序列化后:", string(data)) } }
切片进行序列化
-
将要进行序列化的切片
var slice []int = []int{1,2,3,4,5}
-
切片实例化实例
import "encoding/json" func main() { var slice []int = []int{1,2,3,4,5} data,err := json.Marshal(slice) if err != nil { fmt.Println("序列化失败", err) } else { fmt.Println("序列化后:", string(data)) } }
序列化所需的包以及函数
import "encoding/json"
func Unmarshal(s []byte, v interface{}) (err error)
反序列化为结构体
package main
import "encoding/json"
import "fmt"
func main() {
str := `{"address":"洪崖洞","age":30,"name":"红孩儿"}`
var monster Monster
err := json.Unmarshal([]byte(str), &m)
if err != nil {
fmt.Println("序列化失败", err)
} else {
fmt.Println("序列化后:", m)
}
}
反序列化为map
package main
import "encoding/json"
import "fmt"
func main() {
str := `{"address":"洪崖洞","age":30,"name":"红孩儿"}`
var m map[string]interface{}
m = make(map[string]interface{})
m["one"] = 1
err := json.Unmarshal([]byte(str), &m)
if err != nil {
fmt.Println("序列化失败", err)
} else {
fmt.Println("序列化后:", m)
}
}
反序列化为切片
package main
import "encoding/json"
import "fmt"
func main() {
str := `{"address":"洪崖洞","age":30,"name":"红孩儿"}`
var m map[string]interface{}
err := json.Unmarshal([]byte(str), &m)
if err != nil {
fmt.Println("序列化失败", err)
} else {
fmt.Println("序列化后:", m)
}
}
注意事项
-
结构体在序列化时,非导出变量(小写字母开头)不会被 encode,因此在 decode 的时候这些非导出变量的值为其类型的零值
Part 24 - 反射
反射的概念与作用
- 反射可以在运行时动态获取变量的各种信息,比如变量的类型
- 如果变量是结构体,还可以获取结构体本身的信息,比如结构体的字段,方法
- 通过反射,可以修改变量的值,可以调用变量关联的方法
反射的使用
-
反射包
import "reflect"
reflect.Value
由 reflect.ValueOf(v interface{}) (t reflect.Value) 获取某个变量的 Value
reflect.Value.Kind:获取变量的类别,返回的是一个常量
-
变量,interface{},reflect.Value 之间可以任意转换
// interface{} --> reflect.Value rVal := reflect.ValueOf(v) // reflect.Value --> interface{} iVal := rVal.Interface() // interface{} --> 变量 variable := iVal.(int64) // 变量 --> interface{} iVal = variable
reflect.Type
- 由 reflect.TypeOf(v interface{}) (t reflect.Type) 获取某个变量的 Type
- reflect.Type.Kind:获取变量的类别,返回的是一个常量,与 reflect.Value.Kind 相同
- Type是类型,Kind是类别,它们可能相同,可能不同
- var num int = 10 Type:int Kind:int
- var stu Student Type:包名.Student Kind:struct
Part 25 - 常量
常量用 const 修饰(const替代var)
常量在定义时必须初始化,一旦初始化就不能更改
const只能修饰 bool类型,数值类型,string类型
常量中如果想要使用函数,则只能使用内置函数
常量不能用 := 创建,因为需要 const
-
常量简洁写法
const ( a = 1 b = 2 )
-
常量借助iota实现枚举
const ( // iota是一个内置的变量,用于存储 const 组中常量的个数 a = iota // 0,iota从零开始,组内每定义一个常量,自增1 b // 1 c // 2 ) // 每遇到一个const,iota自动清零 const ( d = iota // 0 e, f = iota, iota // 1, 1 g = iota // 2 )
- Go没有常量必须大写字母开头的规范,因为常量仍然有大小写字母开头控制访问范围的机制
Part 26 - 网络编程
网络编程分类
- 基于 TCP/IP 的 Socket编程
- 基于 HTTP 的 HTTP 编程
端口
- 0是保留端口
- 1-1024是知名端口
- 21:ftp
- 22:ssh
- 23:telnet
- 24:smtp
- 80:http
- 1025-65535是动态端口
Socket 的使用流程
-
服务端
- 监听端口
- 接收客户端发送的 tcp 连接,建立与客户端的 tcp 连接
- 创建 goroutine,处理连接请求
- 关闭连接
-
客户端
- 建立与服务端的连接
- 发送请求
- 接收服务器端返回的处理数据
- 关闭连接
示意图
Socket 实例例子
-
server.go
package main import ( "fmt" "net" ) func process(conn net.Conn) { defer conn.Close() for { buf := make([]byte,1024) n, err := conn.Read(buf) if err != nil { fmt.Println("服务器的err=",err) return } fmt.Println(string(buf[:n])"客户端发送了") } } func main() { fmt.Println("服务器开始监听") // 1. tcp : 表示使用的协议是tcp // 2. 0.0.0.0:8888 :表示监听的端口是8888 listen, err := net.Listen("tcp", "0.0.0.0:8888") if err != nil { fmt.Println("err=", err) return } // 延时关闭资源 defer listen.Close() for { // 等待客户端连接 conn, err := listen.Accept() if err != nil { fmt.Println("err=", err) return } else { fmt.Printf("连接成功,客户端ip=%v",conn.RemoteAddr().String()) } go process(conn) } }
-
client.go
package main import ( "fmt" "net" ) func main() { conn,err := net.Dial("tcp","192.168.20.253:8888") if err != nil { fmt.Println("err=", err) } defer conn.Close() reader := bufio.NewReader(os.Stdin) line, err := reader.ReadString("\n") if err != nil { fmt.Println("err=", err) } n, err := conn.Write([]byte(line)) if err != nil { fmt.Println("err=", err) } fmt.Printf("客户端发送了 %d 个字节,并退出", n) }
Part 27 - 映射
映射类型声明的三种方式
-
仅仅进行声明
var 变量名 map[keyType]valueType
-
声明时直接初始化
var 变量名 map[string]int = make(map[string]int, 10) // 10可以省略 var 变量名 map[string]int = make(map[string]int) // 10可以省略
-
声明时直接赋值
var 变量名 map[string]int = map[string]int{ "one": 1, "two": 2, }
映射 key 和 value 的要求
- key 必须支持 == 运算
- 一般用 string 和 数值 ,还可以用 bool,指针,channel
- 不能用 slice,map和function
- value可以是string,数值,bool,struct,map 不能用slice,map和function
映射的赋值 [增,改]
- m["key"] = value
- 当m中本身没有 key 时,会新增
- 当m中本身有 key 时,会覆盖
- 当空间不够时,会自动扩容
映射的删除 [删]
- delete(m, "one")
- 当删除的 key 不存在时,既不操作也不报错
- Go中没有方法可以一次性清除整个map,如果想,则可以遍历,或者让变量指向一个新的 map,让 gc 把原来那个删除了
映射的查找 [查]
- val := m["one"]
- 访问不存在的 key 值,返回类型默认类型,而不报错
- val, findRes := m["one"]
- 如果找到了 val 为 key 对应值,findRes为 false
- 如果找不到 val 为 类型默认值 ,findRes为true
映射的遍历
不能用 for 循环,因为映射是无序的
-
需要使用 for-range 循环,其中 k 和 v 是拷贝的
for k,v := range m { fmt.Printf("k=%v,v=%v",k,v) }
golang中的map是无序的,每次遍历的结果都可能不一样
映射的排序
golang中的map是无序的,每次遍历的结果都可能不一样
golang中没有专门的方法针对map的key进行排序
-
如果想要对映射排序,则可以先拿出所有的key,将key进行排序,再取出value
var keys []int for key, _ := range m { keys = append(keys, key) } sort.Ints(key)
映射注意事项
- 声明是不会分配内存的,默认值为nil
- 需要使用make来初始化,分配内存后才能使用,make就是给map分配空间
- map的存储是无序的
- 使用内建函数 len 可以获取映射中键值对的个数
- 映射底层的数据结构是哈希表,所以无序
Part 28 - goroutine(协程)
Go的进程和Go的协程
- Go进程:
- 一个Go进程上,可以启动多个协程
- Go协程:
- 有独立栈空间
- 共享堆空间
- 调度由用户定义
- 协程是轻量级线程
- Go协程对比其他语言并发实现的优势
- Go协程是进程开启的,是轻量级的线程,对资源耗费小
- 其他语言的并发一般是基于线程的
- Go协程可以轻松开上万个,而其他语言开上万个并发较为吃力
Go协程的使用
// 编写一个程序,满足以下功能:
// 1. 在进程中,开启一个 goroutine,该协程每隔1秒输出 "hello world"
// 2. 在进程中,也每隔1秒输出 "hello golang",输出10次后,进程结束
// 3. 要求进程和 goroutine 同时执行
package main
import (
"fmt"
"time"
"strconv"
)
func test() {
for i := 0; i < 10; i++ {
fmt.Println("[test] hello world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启一个协程,执行这个 test
for i := 0; i < 10; i++ {
fmt.Println("[main] hello golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
goroutine协程的调度模型
- MPG模型
- M:操作系统上的进程(主线程)
- P:协程需要的上下文
- G:协程
查看和设置Go运行的cpu数目
-
概述
- go1.8以后,默认让程序运行多个核,可以不设置
- go1.8之前,最好进行设置,从而提高性能
-
查看系统的逻辑cpu数目
import "runtime" fmt.Println(runtime.NumCPU())
-
设置执行Go程序的cpu个数
import "runtime" runtime.GOMAXPROCS(num) // 本函数在编译优化时会被去掉
goroutine中使用 recover
-
goroutine中使用 recover,可以解决某个协程中出现 panic,导致整个程序崩溃的问题
defer func() { if err := recover(); err != nil { fmt.Println("发生错误", err) } }
Part 29 - channel
goroutine资源竞争的问题
// 下面程序会发成 资源竞争问题(concurrent map writes)
// 查看是否发生资源竞争问题可以用命令:go build -race test.go
// 现采用goroutine计算1-200各个数的阶乘,并把每个数的阶乘放进map中,最后显示出来
package main
import (
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10)
)
func calculate(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(20 * time.Second) // 避免goroutine还没执行,主线程就结束了
for k,v := range myMap {
fmt.Printf("map[%d]%d\n", k, v)
}
}
解决资源竞争问题 - 通过全局变量加锁
package main
import (
"fmt"
"time"
"sync"
)
var (
myMap = make(map[int]int, 10)
lock sync.Mutex // 全局的互斥锁
)
func calculate(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
lock.Lock() // 加锁
myMap[n] = res
lock.Unlock() // 解锁
}
func main() {
for i := 1; i <= 200; i++ {
go calculate(i)
}
time.Sleep(20 * time.Second) // 避免goroutine还没执行,主线程就结束了
lock.Lock() // 加锁
for k,v := range myMap {
fmt.Printf("map[%d]%d\n", k, v)
}
lock.Unlock() // 解锁
}
解决资源竞争问题 - 通过channel
全局变量锁的缺点
- 主线程等待所有 goroutine 全部完成的时间很难确定
- 通过全局变量锁实现的同步,不利于多个协程对全局变量的读写操作
channel 介绍
- channel 是一个引用类型
- channel的本质是一个队列
- 数据是先入先出的,即多个 goroutine 同时访问时,也不需要加锁
- channel是有类型的,一个string类型的channel只能存放string
channel 声明
var 变量名 chan 数据类型
var intChan chan int
var mapChan chan map[int]string
var perChan chan Person
channel 初始化
- channel 必须初始化后才能使用,即 make 后
var intChan chan int = make(chan int, 3)
channel 写入数据
intChan <- 10
// channel数据放满后就不能再放了,容量不是动态增长的
channel读取数据
var num int
num = <- intChan
// 在没有使用协程的情况下,当channel已经为空,但是依然继续取时,会报 deadlock
channel长度和容量
len(intChan)
cap(intChan)
channel关闭
// 关闭 channel 后,就不能再往里面写数据了,但如果 channel 里面还有数据,则可以继续读取
close(intChan)
channel遍历
// 在遍历时,如果 channel 还没关闭,则会报 deadlock
// 在遍历时,如果 channel 已经关闭,则遍历正常执行
for v := range intChan {
fmt.Println("v=", v)
}
channel阻塞机制
如果只向 channel 中写入数据而不读取,导致 channel 满了,还继续写,就会出现阻塞而 deadlock
如果读的频率远低于写的频率也没有问题,也不会发生 deadlock,关键是要去读
channel 只读与只写
var chan1 chan int // 可读可写
var chan2 chan <- int // 只写
var chan3 <- chan int // 只读
channel与select
// 使用 select 可以解决从管道取数据阻塞的问题
// 1. 定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
// 2. 定义一个管道 5个数据string
strChan := make(chan string, 5)
for i := 0; i < 10; i++ {
intChan <- "hello" + fmt.Sprintf("%d", i)
}
// 在 for-range 遍历管道时,如果不关闭管道,会发生 deadlock 现象
// 但是我们可能不好确定什么时候该关闭管道
// 可以使用 select 方法解决
for {
select {
// 这里,如果管道没有关闭,也不会因为一直阻塞而 deadlock
// 会自动到下一个 case 匹配
case v := <- intChan:
fmt.Println("从 intChan 读取数据%v\n", v)
case v := <- strChan:
fmt.Println("从 strChan 读取数据%v\n", v)
default:
fmt.Println("都没取到")
}
}
通过 channel 解决素数问题
思路图示
代码实现
package main
import (
"fmt"
)
func putNum(intChan chan int) {
for i := 0; i < 1000; i++ {
intChan <- i
}
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
num, ok := <- intChan
if !ok {
break
}
flag = true
for i := 2; i < num; i++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeChan因为取不到数了而退出")
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 1000)
exitChan := make(chan bool, 4)
go putNum(intChan)
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
go func() {
for i := 0; i < 4; i++ {
<- exitChan
}
close(primeNum)
}()
for {
res, ok := <- primeNum
if !ok {
break
}
fmt.Printf("素数:%d\n", res)
}
fmt.Println("main线程退出")
}
Part 30 - go命令行工具
- go 在命令行输入 go,即可查看go命令行工具的说明
- go build 生成可执行二进制文件
- go clean 在go build后会残留一些临时文件,可以用go clean清除
- go run 执行go程序
- go install 类似go build,区别是生成的可执行二进制文件在指定位置
- go get 获取网上的 go 包
- go doc 在线获取包的文档
- go fmt 格式化代码
- go vet 检测代码中的语法错误
- go env 查看当前go环境
Part 31 - Go操作Redis
安装 Go 的 redis 插件
go get github.com/garyburd/redigo/redis
Go 中连接 redis
connect, err := redis.Dial("tcp","localhost:6379")
Go 中执行 redis 命令
_, err := connect.Do("Set", "key1", 998)
result, err := connect.Int(c.Do("Get", "key1")) // 要使用类型断言
Go 中使用 redis 连接池
- 连接池的作用
- 事先初始化一定数量的连接,放入连接池中
- 当Go需要操作redis时,直接从redis连接池中取出连接即可
- 这样可以节省获取redis连接的时间
- 核心代码
var pool *redis.Pool
pool = &redis.Pool {
MaxIdle: 8, // 最大空闲连接数
MaxActive: 0, // 表示和数据库的最大连接数,0表示不限制
IdleTimeout: 100, // 最大空闲时间,单位秒
Dial: func() (redis.Conn, error) { // 初始化连接池的代码
return redis.Dial("tcp", "localhost:6379")
},
}
connect := pool.Get() // 从连接池中取出连接
pool.Close() // 关闭连接池
Part Max - 补充内容
随机数使用
import "rand"
// 设置随机数种子
rand.Seed(time.Now().UnixNano())
// 产生随机数
rand.Intn(100) // 0<=n<100
CGO
- CGO是Go语言调用C代码的模块,是C语言和Go语言之间的桥梁
- 原则上无法直接支持 C++ 的类,因为C++至今为止都没有一个二进制接口规范
VS Code 使用技巧
反射再看一遍
网络再看一遍
有疑问加站长微信联系(非本文作者)