Golang
数据类型
布尔类型(bool)
1. 布尔类型也叫 bool 类型,bool 类型数据只允许取值 ture 和 flase
2. bool 类型占 1个字节
3. bool 类型适用与逻辑运算,一般用于程序流程控制
数字类型
int , float32, float64 , Go 语言支持整型和浮点数字,并且支持复数,其中位运算采用补码
数字类型
uint8
无符号 8 位整性 0 - 256
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)
浮点类型
float32
IEEE-754 32位浮点型数
float64
IEEE-754 64位浮点型数
complex64
32 位实数和虚数
complex128
64 位实数和虚数
字符串类型(string)
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。
Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 Go 的字符串不同,它是由字节组成的。
注意事项(使用细节)
1. Golang 字符串的字节使用 UTF-8编码进行表示 Unicode 文本,这样golang 统一适用了 utf-8 变么,就能避免了中文乱码的问题
2. 字符串一旦定义就不能修改
错误案例:
var str1 string = "124"
str1[1] = "3"
3.字符串的两种形式
1) 双引号,会识别转义字符
2)反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击,输出源代码等效果
s1 := `
func printBool() {
var b bool = false
fmt.Println("b=", b)
fmt.Println("b 占用的空间 = ", unsafe.Sizeof(b))
}`
fmt.Println(s1)
//输出结果
func printBool() {
var b bool = false
fmt.Println("b=", b)
fmt.Println("b 占用的空间 = ", unsafe.Sizeof(b))
}
4.字符串拼接的方式
1)通过 + 加号链接
5. 字符串多行拼接的时候, 换行的时候 + 加号需要放到前行的尾部
str := "hello" + "world"+
"hello " + "world" +
"hello " + "world"
fmt.Println(str)
//输出结果
helloworldhello worldhello world
字符类型(char)
1. 字符常量是用单引号(\'\')括起来的单个字符,例如 var c1 char = \'a\', var c2 char = \'中\' var c3 char = \'9\'
2. Go 中有女婿使用转义字符 \'\\' 来将后面的字符串转变为特殊字符类型常量。例如:var c3 = \'\n\' // \'\n\' 表示换行符
3. Go 语言的字符串使用 UTF-8(英文字母1个字节,汉字3个字节)
var a int = 22269
fmt.Printf("%c", a)
//输出结果
国
4. 在 Go中, 字符的本质是一个整数, 直接输出的时,应该是字符对应的 UTF-8 码值。
5. 可以直接给某个变量赋一个数字,然后按照格式化输出时 %c, 会输出该字符串对饮的 unicode 字符
6. 字符类型可以进行运算的,相当于一个函数,因为他们都有对应的 Unicode 码
字符类型的本质
1. 字符型 存储到计算机中,需要讲字符对应的码值(整数)找出来
存储:字符串 -> 对应码值--> 二进制 --> 存储
读取:二进制 --> 码值 --> 字符 --> 读取
2. 字符和码值的对应关系是通过字符编码表决定的(是规定好的)
2.Go语言的编码都统一成了 utf-8 . 非常方便,很统一,不会有编码乱码的困扰
其他数字类型
byte
类似 uint8
rune
类似 int32
uint
32 或 64 位
int
与 uint 一样
uintptr
无符号整形,用于存放指针
基本数据类型的默认值
整型 ==> 0
浮点型 ==> 0
字符串 ==> ""
布尔类型 false
复合类型
指针:pointer:存储的是变量的内存地址
声明和初始化
var 指针名 *变量类型
var ptr *int
获取指针值
*指针名
*ptr
指针不能运算
数组:array 值类型
声明和初始化
var 数组名 [长度] 类型
var arr [8]int
var 变量名 = [长度]类型{值, 值...}
var arr = [3]int{1, 2, 3}
数组名 := [...]类型{值, 值...}
arr := [...]int{1, 2, 3}
数组名 := [长度]类型{下标:值...}
arr := [3]int{0:1, 2, 3}
注意事项
长度不一样,类型就不一样
第一个元素的内存地址,是这个数组的内存地址
数组长度需要在编译期确定
切片:slice 引用类型
var 切片名 []类型 = []类型{值, 值...}
var sli []int = []int{1, 2, 3}
基于数组创建
切片名 := 数组名[开始位置:结束位置]
months := [...]string{"a", "b", "c", "d", "e", "f","g", "h", "i", "j", "k", "l"}
q2 := months[3:6]
fmt.Println(q2) //输出 [d e f]
fmt.Println(len(q2)) //输出 3
fmt.Println(cap(q2)) //输出 9
直接创建
切片名 := make([]类型, [容量])
sli := make([]int, 10)
动态增加元素
新切片名 := append(旧切片名, 值)
var oldSlice = make([]int, 5, 10)
newSlice := append(oldSlice, 1, 2, 3)
fmt.Println(newSlice) //[0 0 0 0 0 1 2 3]
新切片名 := append(旧切片名, 旧切片名...)
var oldSlice = make([]int, 5, 10)
appendSlice := []int{3, 4, 5}
newSlice := append(oldSlice, appendSlice...)
fmt.Println(newSlice) //[0 0 0 0 0 3 4 5]
内容复制
copy(切片名1, 切片名2)
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{6, 7, 8}
//复制slice2到slice1 只会复制slice1的前三个元素到slice2中
copy(slice2, slice1)
fmt.Println(slice1) //输出[6 7 8 4 5]
//复制slice1到slice2 只会复制slice2的三个元素到slice1的前三个位置
copy(slice1, slice2)
fmt.Println(slice2) //输出[1 2 3]
注意事项:
1、如果切片的大小不一样,则会按最小的那个进行复制
动态删除
切片名 = 切片名[start:end]
slice1 := []int{1, 2, 3, 4, 5}
slice1[1:2]
字典:map 引用类型
var 字典名 map[类型]类型
var testMap map[string]int
注意:
1、map是个无序的集合,每次编译后运行显示的顺序都是不一样的
2、如果仅仅是声明,初始化map之前一定要make,不然会报错
初始化
1、
var testMap map[string]int
testMap = map[string]int{
"one":1,
"two":2,
}
2、
testMap := map[string]int{
"one":1,
"two":2,
}
3、
var testMap = make(map[string]int)
testMap["one"] = 1
testMap["two"] = 2
testMap["three"] = 3
查找元素:值, ok := 字典名["下标"]
testMap := map[string]int{
"one":1,
"two":2,
}
value, ok := testMap["one"]
if ok {
}
删除元素:delete(切片名, 下标)
testMap := map[string]int{
"one":1,
"two":2,
}
delete(testMap, "one")
通道:chan 引用类型
结构体:struct 值类型
接口:interface
定义方法集,但不实现这些方法,不能包含属性
type Nember interfanc {
method(param_list) return_type
...
}
注意事项
接口名由方法名+(e)r或者以I开头
任何类型都可以实现接口
实现了接口的所有方法,就是这个接口的实例
类型转换
// byte转数字
s="12345" // s[0] 类型是byte
num:=int(s[0]-\'0\') // 1
str:=string(s[0]) // "1"
b:=byte(num+\'0\') // \'1\'
fmt.Printf("%d%s%c\n", num, str, b) // 111
// 字符串转数字
num,_:=strconv.Atoi()
str:=strconv.Itoa()
变量和常量
const 常量名 常量类型 = 值
const 常量名 = 值
注意事项
只能是布尔型、整型、浮点型、复数、字符串
值要在编译时就可以确定的
const a = 2/3 //正确做法
const a = getNumber() //错误做法
预定义常量
iota:初始值为0 每出现一次就自动增1 遇到const时会被重置为0
变量声明和初始化
var 变量名 变量类型; 变量名 = 值
var 变量名 = 值
var(变量名1 变量类型; 变量名2 变量类型)
变量名 := 值
变量名1, 变量名2 = 变量名2, 变量名1
注意事项
1、使用":="的变量一定不能被声明过的,否则报错
2、":="只能在函数中使用
3、多个变量赋值,只要有一个是没被声明过的,就可以使用":="
4、局部变量声明之后,如果未被使用则会报错,全局变量则不会
init函数:包初始化之后自动执行,优先级比main函数高。每个源文件只能包含一个
变量的作用域
1. 函数内部声明 / 定义的变量叫做局部变量, 作用域仅局限于函数内部。
2. 函数外部声明/ 定义的变量叫做全局变量, 作用于在整个包都有效,如果首字母为大写,则作用于在整个程序中都有效
3. 如果变量是在一个代码块, 比如 for / if 中,那么这个变量的作用域在该代码块
变量作用于分析
package main
import "fmt"
var name = "tom"
func test01() {
fmt.Println(name)
}
func test02() {
name := "jack"
fmt.Println(name)
}
func main() {
fmt.Println(name) //tom
test01() //tom
test02() //jack
test01() //tom
}
函数是基本的代码块,用于执行一个任务。
Go 语言最少有个 main() 函数。
你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。
函数声明(函数签名)告诉了编译器函数的名称,返回类型,和参数。
Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。
如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
函数的命名遵循标识符规范,首字母不能是数字,手写字母答谢该函数可以被本包文件和其它包文件使用, 类似 public , 首字母小写, 只能被本包文件使用, 其它包文件不能使用, 类似 private
不支持重载
函数定义如下:
fun function_name ([parameter list]) [return_types] {
函数体
}
简单函数调用
package main
import "fmt"
func main() {
fmt.Println("a+b=", test(1,2))
}
func test(a, b int) int {
return a + b
}
函数返回多个值
package main
import "fmt"
func main() {
a, b := test(1, 2);
fmt.Println(a, b)
}
func test(a, b int) (int, int) {
return a, b
}
函数参数的传递方式
基本介绍
我们在讲解函数注意事项和基使用细节时,已经讲过值类型和引用类类型了,这里我们在系统的总结一下。
因为这里是重难点, 值类型参数默认就是值传递。 而引用类型参数默认就是引用传递。
两种传递方式
1. 值传递
2. 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是, 值传递的是值的拷贝,
引用传递的地址的拷贝,一般来说,地址拷贝效率高,因为数量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低
值类型和引用类型
1. 值类型,基本数据类型 int 系列, float 系列, bool, string, 数组和结构体 struct
2. 引用类型, 指针, slice 切片、map、 管道 chan 、interface 等都是引用类型
值传递和引用传递的使用特点
1. 值类型默认是值传递,变量直接存储值,内存通常在栈上分配
2. 引用类型默认是引用传递,变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,
当没有任何变量引用这个地址时,该地址对应的数据空间就成为了一个垃圾,由 GC 来回收。
值类型和引用类型
值类型:基本数据类型 int 系列,bool , string , 数组和结构体 struct
引用类型:指针,slice 切片, map , 管道 chan , interface 等都是引用类型
使用特点
1. 值类型,变量直接存储值,通常在栈中分配
2. 引用类型,变量存储的是一个地址,这个地址空间才是真正存储数据(值),内存通常在堆上分配,
当没有任何变化引用这个地址时,该地址的数据空间就成为一个垃圾,由 GC 来回收
make 和 new
new:作用于值类型,仅分配内存空间,返回指针
make:作用于引用类型,分配内存空间和初始化,返回初始值
标识符
1. 由 26 个英文字母大小写,0-9, _ 组成
2. 数字不可以开头
3. Golang 中严格区分大小写
4. 标识符不能包含空格
5. 下划线 “_”本身在 Go 中是一个特殊的标识符,成为空标识符。可以代表任何其他的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。
所以仅能够被作为占位符使用,不能作为标识符使用
6. 不能以系统保留字作为标识符,比如 : brack , if 等
7. 如果变量名、函数名、长两名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包内使用
包的使用
包的三大作用
1. 区分相同文件的函数,变量等标识符
2. 当程序文件很多的时候,可以很好的管理项目
3.控制函数。变量等访问范围,即作用域
包的相关说明
包的基本语法
package util
引入包的基本语法
import "包的路径"
包的使用注意事项和细节说明
1)在给一个文件打包的时候,该包对应一个文件夹,比如这里的 utils 文件夹对应的包就是util,
通常和文件所在的文件名保持一致,一般为小写字母。
2)当一个文件用使用其它包函数或变量时,需要先引入对应的包
a. 引入方式 1 : import "包名"
b. 引入方式 2:
import (
"包名"
)
c. package 指令在文件第一行,然后是 import 指令
d. 在 import 包时, 路径从 $GOPATH 的 src 下开始, 不用带 src , 编译器会自动从 src 下开始引入
3)为了让其它包的文件,可以访问到本包的函数、变量,则该函数名的首字母需要大写,类似其它语言的 public , 这样才能跨包访问。
4)在访问其它包函数时,其语法是 包名.函数名
5)如果包名比较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名不能使用了
6) 如果在同一个包下不能有同名的函数,这样会导致编译报错
7)如果你需要编译成可执行文件,就需要将这个包声明为 main, 即 pachage main 这就是一个语法规则, 如何你就是写一个库, 包名可以自定义
init 函数
基本介绍
每一个源文件都包含一个 init 函数, 该函数会在main 函数执行之前被执行
细节讨论
1. 如果同一个文件中同时是包含全局变量定义, init 函数和main 函数, 则执行的流程是 变量定义 -> init 函数 -> main 函数
2. init 函数的主要的作用, 就是完成一些初始化的工作,比如下面的案例
案例代码
//utils.go
package utils
import (
"fmt"
)
var Age int
var Name string
func init() {
fmt.Println("utils.init()...")//1
Age = 10
Name = "张三"
}
//main.go
package main
import (
"fmt"
"TestDemo/funcdemo/funcinit/utils"
)
var age = test()
//为了看到全局变量是先被初始化的,我们先写一个函数给全局变量 age 提供值
func test() int {
fmt.Println("test()...")//2
return 90
}
//init 函数,通常可以在init 函数中完成初始化的工作
func init() {
fmt.Println("init()...")//3
}
func main() {
fmt.Println("main()...")
fmt.Printf("Age = %v, Name = %v", utils.Age, utils.Name)//4
}
3. 如果 main.go 和 utils.go 都包含变量定义, init 函数时,执行的流程是怎么样的
示意图
函数中的 - defer
为什么需要 defer
在函数中, 程序员常需要创建资源(比如,数据库链接、文件句柄,锁等)为了在函数的执行完毕后,及时的释放资源,
Go 的设计者提供了 defer (延迟机制)。
细节说明
1. 当 go 执行到一个 defer 时, 不会立即执行 defer 的语句,而是讲 defer 语句压入到一个栈中, 然后继续执行函数的下一个语句
2. 当函数执行完毕后, 在 defer 栈中,依次从栈顶取出语句执行(注,遵循栈先入后出的机制),所以看到上面的输入顺序
3. 在 defer 讲语句放入到栈时,也会将相关的值拷贝同时入栈。
演示案例
package main
import (
"fmt"
)
func sum(n1 int, n2 int) int {
//当执行到 defer 时, 会将 defer 后面的语句压入到独立的栈(defer 栈)
//当前函数执行完毕后,再从 defer 栈,按照先入后厨的方法栈,执行
defer fmt.Println("ok1 n1=", n1) //3
defer fmt.Println("ok2 n2=", n2) //2
n1++
n2++
res := n1 + n2
fmt.Println("ok3 res=", res) //1
return res
}
func main() {
sum := sum(1, 2)
fmt.Println("sum=", sum) //4
}
//输出结果
ok3 res= 5
ok2 n2= 2
ok1 n1= 1
sum= 5
defer 的最佳实践
defer 最主要的价值是在, 当函数执行完毕后, 可以及时释放函数创建的资源
1. 在 golang 编程中通常的做法是, 创建资源后, 比如(打开了文件,获取了数据库链接,或者锁资源)
可以执行 defer file.Close() , defer connect.Close()
2. 在 defer 后, 可以继续使用创建资源
3. 当函数完毕后, 系统会依次从 defer 栈中,取出语句,关闭资源
4. 这种设计机制非常简介,程序员不用在等什么时机关闭资源而烦心
字符串中常用的函数
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
//1. 统计字符串涨肚, 按字节 len(str)
//golang 的编码统一为 utf-8 ( ascii 字符 (字母和数字)占一个字节, 1个汉字占三个字节)
var str = "hello北"
fmt.Println("str len=", len(str))
//2. 字符遍历,同时处理中文的问题 r: []rune(str)
var str2 = "Hello北京"
str3 := []rune(str2)
for i := 0; i<len(str3); i++ {
fmt.Printf("i = %v, s = %c\\n", i, str3[i])
}
//3. 字符串转整数: n, err := strconv.Atoi("12")
n, err := strconv.Atoi("12A3")
if err != nil {
fmt.Println("转换错误", err)
} else {
fmt.Println("转换结果", n)
}
//4. 整数转换为字符串 str = strconv.Itoa(123456)
str1 := strconv.Itoa(1212121212112121212)
fmt.Printf("str=%v, str=%T\\n", str1, str1)
//5. 字符串转 []byte : var bytes = []byte("hello go")
bytes := []byte("hello go")
fmt.Printf("bytes=%v\\n", bytes)
//6. []byte 字符串: str = string([]byte{97, 98, 99})
str4 := string([]byte{97, 98, 99})
fmt.Printf("str4=%v\\n", str4)
//7. 10 进制转 2,8,16: str = strconv.FormartInt(123, 2)
str = strconv.FormatInt(123, 2)
fmt.Printf("123 对应的二进制是 = %v\\n",str)
str = strconv.FormatInt(123, 16)
fmt.Printf("123 对应的十六进制是 = %v\\n",str)
//8. 查找字符串是否存在制定的子串: strings.Contains("abssdb", "ab111") //true
b := strings.Contains("absadasdsad", "abs11")
fmt.Printf("b = %v\\n",b)
//9. 统计一个字符串有几个指定的子串:strings.Count("ceheese", "e") //4
n = strings.Count("che22eeasasada", "e")
fmt.Printf("n = %v\\n",n)
//10. 不区分大小写的字符串比较(== 是区分字母大小写的):fmt.Println(strings.EqualFold("abc", "Abc")) //true
b = strings.EqualFold("Abc", "ab1C")
fmt.Printf("b = %v\\n",b)
fmt.Printf("abc == Abc => %v\\n", "abc" == "Abc")
//11. 返回子串在字符串第一次出现的 index 值, 如果没有值返回 -1
idx := strings.Index("ASadsdas_abc_|abc", "abc")
fmt.Printf("idx = %v\\n",idx)
//12. 返回子串在字符串最后出现的 index , 如果没有返回 -1: strings.LastIndex("go lang", "go")
idx = strings.LastIndex("go golang", "go")
fmt.Printf("index=%v\\n", idx) //3
//13. 将制定的字符串替换为另外一个子串:strings.Replace("go go hello", "go", "go 语言", n) n 可以制定你喜欢替换几个, 如果 n = -1 表示全部替换
str2 = "go go hello"
str = strings.Replace(str2, "go", "北京", 1)
fmt.Printf("str=%v, str2=%v\\n", str, str2) //str=北京 go hello, str2=go go hello
//14. 按照制定的某个字符,为分割标识,讲一个字符串拆分成字符串数组
//strings.Split("Hello, Wrold!", ",")
strArr := strings.Split("hello, world, ok", ",")
for i :=0; i < len(strArr); i++ {
fmt.Printf("str[%v]=%v\\n", i, strArr[i])
}
fmt.Println(strArr)
//15. 将字符串的字母进行大小写的转换:strings.ToLower("GO") // go strings.ToUpper("Go") //GO
str6 := "GoLang Hello"
str = strings.ToLower(str6)
fmt.Println(str)
str = strings.ToUpper(str6)
fmt.Println(str)
//16. 将字符串左右两边的空格去掉: strings.TrimSpace("tn a lone gopher ntrn ")
str = " this is a apple"
str = strings.TrimSpace(str)
fmt.Printf("str=%q\\n", str)
//17.将字符串左右两边的指定字符去掉:strings.Trim("! hello!", " !")//将左右两边 ! 和 "" 去掉
str = strings.Trim("! he!llo !", " !")
fmt.Printf("str=%q\\n", str)
//18. 将字符串左边指定的字符去掉: strings.TrimLeft("! hello !", " !") //将左边 ! 和 "" 去掉
//19. 将字符串右边指定的字符去掉: strings.TrimRight("! hello !", " !") //将右边 ! 和 "" 去掉
//20. 判断字符串是否以指定的字符串开头:strings.HasPrefix("ftp://192.168.1.1","ftp")
//true
b = strings.HasPrefix("ftp://192.168.1.1","ftp")
fmt.Printf("str=%v\\n", b)
//21. 判断字符串是否以指定的只服从结束:strings.HasSuffix("a.jpg", "jpg") //true
b = strings.HasSuffix("a.jpg","jpg")
fmt.Printf("str=%v\\n", b)
}
字符串在我们程序开发中,使用非常的多,常用的函数需要掌握
1. 统计字符串长度, 按字节 len(str)
2. 字符串遍历,同时处理有中文的问题, r := []rune(str)
3. 字符串转整数: n, err := strconv.Atoi("12")
4. 整数转换为字符串 str = strconv.Itoa(123456)
5. 字符串转 []byte : var bytes = []byte("hello go")
6. []byte 字符串: str = string([]byte{97, 98, 99})
7. 10 进制转 2,8,16: str = strconv.FormartInt(123, 2)
8. 查找字符串是否存在制定的子串: strings.Contains("abssdb", "ab") //true
9. 统计一个字符串有几个指定的子串:strings.Count("ceheese", "e") //4
10. 不区分大小写的字符串比较(== 是区分字母大小写的):fmt.Println(string2.EqualFold("abc", "Abc")) //true
11. 返回子串在字符串第一次出现的 index 值, 如果没有值返回 -1
strings.Index("NLT_abc", "abc" ) //4
12. 返回子串在字符串最后出现的 index , 如果没有返回 -1: strings.LastIndex("go lang", "go")
13. 将制定的字符串替换为另外一个子串:strings.Replace("go go hello", "go", "go 语言", n) n 可以制定你喜欢替换几个, 如果 n = -1 表示全部替换
14. 按照制定的某个字符,为分割标识,讲一个字符串拆分成字符串数组
strings.Split("Hello, Wrold!", ",")
15. 将字符串的字母进行大小写的转换:strings.ToLower("GO") // go strings.ToUpper("Go") //GO
16. 将字符串左右两边的空格去掉: strings.TrimSpace("tn a lone gopher ntrn ")
17.将字符串左右两边的指定字符去掉:strings.Trim("! hello!", " !")//将左右两边 ! 和 "" 去掉
18. 将字符串左边指定的字符去掉: strings.TrimLeft("! hello !", " !") //将左边 ! 和 "" 去掉
19. 将字符串右边指定的字符去掉: strings.TrimRight("! hello !", " !") //将右边 ! 和 "" 去掉
20. 判断字符串是否以指定的字符串开头:strings.HasPrefix("ftp://192.168.1.1","ftp")
//true
21. 判断字符串是否以指定的只服从结束:strings.HasSuffix("a.jpg", "jpg") //true
错误处理
错误处理总结
1. 在默认情况下,当发生错误后(panic), 程序就会退出(崩溃)
2. 如果我们希望,发生错误后,可以捕获错误,并进行处理,保证程序可以继续执行。
还可以捕获到错误后,给管理员一个提示(邮件、短信。。。)
基本说明
1. Go 语言最求简洁优雅, 所以 , Go 语言不支持传统的 try ... catch.. finally 这种处理,。
2. Go 中引入的处理方式为:defer , panic , recover
3. 这几个异常的是采用场景可以这么简单描述:Go中可以抛出 一个 panic 的异常,然后在 defer 中通过 recover 捕获异常,然后正常处理
自定义错误
Go 程序中, 也支持自定义错误, 使用 errors.New , 和 painc 内置函数
1. errors.New("错误说明"), 会返回一个 error 类型的值 , 表示一个错误
2. panic 内置函数, 接受一个 interface {} 类型的值(也就是任何值了)作为参数,
可以接受 error 类型的值, 输出错误信息, 并且退出程序。
访问数组的元素
数组的4种初始化的方式
//四种的初始化方式
var numArr01 [3]int= [3]int{0,1,3}
fmt.Println("numArr01=",numArr01)
var numArr02 = [3]int{0,1,3}
fmt.Println("numArr02=",numArr02)
var numArr03 = [...]int{0,1,3}
fmt.Println("numArr03=",numArr03)
var numArr04 = [...]int{1:800, 0:900, 2:123}
fmt.Println("numArr04=",numArr04)
strArr05 := [...]string{1:"tom", 2:"jack", 0:"12112"}
fmt.Println("strArr05=",strArr05)
数组的遍历方式
1. 常规遍历
2. for-range 结构遍历
for index, value : range array01 {
....
}
1. 第一个返回值是 index 的下标
2. 第二个 value 是在该下标位置的值
3. 他们都仅在 for 循环内部可见的局部变量
4. 遍历数组元素的时候,如果不想使用下标 index,可以直接把下标为 _ 接收表示忽略
5. index, value 的名称不是固定的,程序员可以自己定义,一般命名为index 和 value
数组的使用细节
package main
import (
"fmt"
)
func main() {
//for-range 遍历数组
var herose [3]string = [3]string{"张三", "李四", "王五"}
for i, v := range herose {
fmt.Printf("i=%v, v=%v\\n", i, v)
fmt.Printf("herose[%d]=%v\\n", i, herose[i])
}
var arr = [3]int{11, 22, 33}
fmt.Printf("arr 的地址 = %p\\n", &arr)
// test01(arr)
test02(&arr)
fmt.Println("arr =>", arr)
}
func test01(arr [3]int) {
fmt.Printf("arr 的地址 = %p\\n", &arr)
arr[0] = 88
}
func test02(arr *[3]int) {
fmt.Printf("arr 的地址 = %p\\n", &arr)
(*arr)[0] = 99
}
1. 数组是多个相同类型的数据集合, 一个数组一旦声明/定义了,其长度是固定的,不能动态改变
2. var arr []int 这时 arr 就是一个 slice 切片, 切片后面会提到
3. 数组的元素可以是任意类型,包括值类型和引用类型, 但是不能混用
4. 数组创建后如果没有赋值, 有默认值
数值类型数组 : 默认为0
字符串数组:默认为""
bool 数组:默认值为 false
5. 使用步骤 1. 声明数组病开辟空间,2,给数组的各个元素赋值 3 使用数组
6. 数组的下表是 0 开始的
7. 数组的下标必须在指定的范围内使用,否者报 panic , 数组越界,比如: var arr [5]int 则有效下表为 0-4
8. Go 的数组属于值类型,在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
9. 如果想在其他函数中,取修改原来的数组,可以使用引用传递(指针方式)
10. 长度是数组类型的一部分, 在传递参数的时候需要考虑数的长度
切片的基本介绍
package main
import (
"fmt"
)
func main() {
//演示切片的基本使用
var intArr [5]int = [...]int{1, 22, 33, 66, 55}
//声明/定义一个切片
//slice := intArr[1:3]
//1. slice 是切片的名称
//2. intArr[1:3] 表示 slice 引用到 intArr 这个数组的第 2 个元素到第 3 个元素
slice := intArr[1:3]
fmt.Println("intArr=", intArr)
fmt.Println("slice 的元素是=", slice)
fmt.Println("slice 的长度是=", len(slice))
fmt.Println("slice 的容量是=", cap(slice)) //切片的容量可以动态变化
}
//执行结果
intArr= [1 22 33 66 55]
slice 的元素是= [22 33]
slice 的长度是= 2
slice 的容量是= 4
切片是数组的一个引用, 因此切片是引用类型,在进行传递时,遵循引用传递的机制
切片的使用和数组类似,遍历切片、访问切片的元素和长度 len(slice) 都一样
切片的长度是可以变化的, 因此切片可以是一个动态变化的数组
切片的基本定义语法:
var 变量名 [] 类型
举例:var a []int
切片使用的三种方式
1. 定义一个切片,然后让切片取引用一个已经创建好的数组,比如前面的案例就是这样的。
2. 第二种方式: 通过 make 来创建切片,基本语法: var 切片名 []type = make([], len, cap)
参数说明: type 就是数据类型, len: 大小, cap: 制定切片容量,可选
1. 通过 make 方式创建可以制定切片的大小和容量
2. 如果没有给切片的各个元素赋值, 那么就会使用默认值 [ int, float = 0, string = "", bool= false ]
3. 通过 make 方式创建的切片对应的数组是由 make 底层维护的,对外不可见,即只能通过 slice 去访问
3. 第三种方式, 定义一个切片,直接制定具体数组 ,使用原理类似make 的方式
方式 1 和方式 2 的区别
1. 直接的方式是引用数组, 这个数组是事先存在的, 程序员可见的。
2. 方式二,通过 make 来创建切片, make 也会创建一个数组, 是由切片在底层进行维护, 程序员是看不见的。
切片的遍历两种方式
package main
import (
"fmt"
)
func main() {
//使用常规的方式进行遍历切片
var arr [5]int = [...]int{10,20,30,40,50}
slice := arr[1:4]
for i := 0; i< len(slice); i++ {
fmt.Println(slice[i])
}
fmt.Println("---------------")
//使用for ... range 来遍历切片
for _, v := range slice {
fmt.Println(v)
}
}
1. 使用常规的 for 循环遍历
2. 使用for ... range 来遍历切片
切片的细节说明
1. 初始化时 var slice = arr[startIndex:endIndex]
说明: 从arr 数组下标为 startIndex, 取到下表为 endIndex 的元素(不含 arr[endIndex])
2. 切片初始化时, 不能越界, 范围在[0, len -1] 之间,但是可以动态增长
1). var slice = arr[0:end] 可以简写 var slice = arr[:end]
2). var slice = arr[start:len(arr)] 可以简写 var slice = arr[start:]
3) var slice = arr[0:len(arr)] 可以简写 var slice = arr[:]
3. cap 是一个内置函数, 用于统计切片的容量, 即最大可以存放多少个元素
4. 切片定义后, 不能使用,因为本身是一个空的, 需要让其引用到一个数组或者 make 一个空间供切片使用
5. 切片可以继续切片
演示案例
package main
import (
"fmt"
)
func main() {
//使用常规的方式进行遍历切片
var arr [5]int = [...]int{10,20,30,40,50}
slice := arr[1:4]
for i := 0; i< len(slice); i++ {
fmt.Println(slice[i])
}
fmt.Println("---------------")
//使用for ... range 来遍历切片
for _, v := range slice {
fmt.Println(v)
}
//对切片进行再次切片
slice2 := slice[1:2]
slice2[0] = 100 //因为 arr, slice, slice2 指向的是同一块数据空间, slice2 改变了,其他的都要变化
fmt.Println("arr=", arr)
fmt.Println("slice=", slice)
fmt.Println("slice2=", slice2)
}
6. 使用 append 内置函数, 可以对切片进行动态追加
package main
import (
"fmt"
)
func main() {
//使用常规的方式进行遍历切片
var arr [5]int = [...]int{10,20,30,40,50}
slice := arr[1:4]
for i := 0; i< len(slice); i++ {
fmt.Println(slice[i])
}
fmt.Println("---------------")
//使用for ... range 来遍历切片
for _, v := range slice {
fmt.Println(v)
}
//对切片进行再次切片
slice2 := slice[1:2]
slice2[0] = 100 //因为 arr, slice, slice2 指向的是同一块数据空间, slice2 改变了,其他的都要变化
fmt.Println("arr=", arr)
fmt.Println("slice=", slice)
fmt.Println("slice2=", slice2)
//使用 append 内置函数,可以堆切片进行动态追加
var slice3 []int = []int {100, 200, 300}
fmt.Println("slice3=", slice3)
//通过 append 直接给 slice3 追加具体的元素
slice3 = append(slice3, 400 , 500)
//通过 append 追加到切片 append
slice3 = append(slice3, slice3...)
fmt.Println("slice3=", slice3)
}
append 操作的底层原理分析如下:
1. 切片 append 操作的本质是对数组拓容
2. go 底层会创建一个新的数组 new Arr(按照拓容后的大小)
3. 将 slice 原来包含的元素拷贝到新的数组 newArr
4. slice 重新引用到 newArr
5. 注意 newArr 是在底层来维护的, 程序员看不见
7. 切片拷贝操作
切片使用 copy 内置函数完成拷贝
copy(para1,para2) : para1 和para2 都是切片类型
案例代码
//切片使用 copy 内置函数完成拷贝
var slice4 []int = []int {1, 2, 3, 4}
var slice5 = make([]int, 10)
copy(slice5, slice4)
fmt.Println("slice4", slice4)
fmt.Println("slice5", slice5)
slice4, slice 5 的空间是相互独立的, 修改 slice4[0] = 5 那么 slice5 不会改变
代码分析
var a []int = []int {1, 2, 3, 4,5 }
var slice = make([]int, 1)
fmt.Println(slice) //[0]
copy(slice, a)
fmt.Println(slice) //[1]
8. 切片是引用类型, 所以在传递的时候, 准守引用传递机制。
案例 1
func main() {
var slice []int
var arr [5]int = [...]int{1,2,3,4,5}
slice = arr[:]
var slice2 = slice
slice2[0] = 10
fmt.Println("slice2",slice2) //[10 2 3 4 5]
fmt.Println("slice",slice) //[10 2 3 4 5]
fmt.Println("arr",arr) //[10 2 3 4 5]
}
案例 2
package main
import (
"fmt"
)
func main() {
var slice = []int{1, 2, 3, 4}
fmt.Println("slice=", slice) //[1, 2, 3, 4]
test(slice)
fmt.Println("slice=", slice) //[100, 2, 3, 4]
}
func test(slice []int) {
slice[0] = 100
}
9. 切片只能和 nil 进行等值判断
map 介绍
map 是 key - value 数据结构, 又称为字段或关联数组, 类似其他变成语言的集合
map 的申明
var map 变量名 map[keytype][valuetype]
key 可以是什么类型: golang 中 map 的 key 可以是很多种类型, 比如 bool, 数字, string , 指针, channel , 还可以是只包含前面几个类型的借口,结构体, 数组, 通常是 int, string
注意:slice , map,还有 function, 不可以因为这几个类型没有办法用 == 比较
一般 keytyp 的类型是: 整数、浮点数、string, map, struct
创建的案例
package main
import (
"fmt"
)
func main() {
var a map[string]string
//使用map 之前需要先申明数据空间, 就是为了先为它申明空间
a = make(map[string]string, 10)
a["no1"] = "松江"
a["no2"] = "武松"
a["no1"] = "吴用"
a["no3"] = "李逵"
fmt.Println(a)
}
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]string
var a map[string]map[string]string
注意:申明的时候不会进行内存分配的初始化需要 make , 分配内存后才能赋值和使用
申明总结
1. map 在使用之前需要 make 分配空间
2. map 的 key 是不能重复的, 如果重复了将覆盖之前设置的值
3. value 在不同的 key 下面是可以重复的
4. map 的 key-value 是无序的
map 的使用
三种使用方式
package main
import (
"fmt"
)
func main() {
var a map[string]string
//1.使用map 之前需要先申明数据空间, 就是为了先为它申明空间
a = make(map[string]string, 10)
a["no1"] = "松江"
a["no2"] = "武松"
a["no1"] = "吴用"
a["no3"] = "李逵"
fmt.Println(a)
//第二种方式
b := make(map[string]string)
b["no1"] = "北京"
b["no2"] = "上海"
fmt.Println(b)
//第三种方式
var c map[string]string = map[string]string{
"no1" : "松江",
"no2" : "长江",
}
fmt.Println(c)
}
1. 先申明后make
2. 申明时候 make 分配空间
3. 直接申明和赋值
案例演示: 一个 key-value 的 value 是 map 的案例, 比如:我们需要存放是哪个学生信息,每个学生信息有 name 和 sex 信息
map 的 crud 操作
1. map 删除, 使用 delete 函数
示例代码
cities := make(map[string]string)
cities["no01"] = "北京"
cities["no02"] = "上海"
cities["no03"] = "天津"
//删除 map 中的一个元素
delete(cities, "no01")
//如果不存在删除也不会报错
delete(cities, "no04")
fmt.Println(cities)
删除细节
1. 如果我们要删除 map 的所有 key, 没有一个专门的方法进行一次删除, 可以遍历 key 逐条删除
2. 或者 map := make(...) make 一个新的, 让原来的成为垃圾被 gc 回收
2. map 查找
查找案例
//map 的查找
\tval, ok := cities["no01"]
\tif ok {
\t\tfmt.Println("有no01值是", val)
\t}
map 遍历
package main
import (
"fmt"
)
func main() {
//使用 for - range 遍历 map
cities := make(map[string]string, 10)
cities["no1"] = "北京"
cities["no2"] = "天津"
for k, v := range cities {
fmt.Printf("k=%v, v=%v\\n", k, v)
}
stuMap := make(map[string]map[string]string, 10)
stuMap["stu01"] = make(map[string]string, 3)
stuMap["stu01"]["name"] = "Tom"
stuMap["stu01"]["sex"] = "男"
stuMap["stu01"]["address"] = "北京"
stuMap["stu02"] = make(map[string]string, 3)
stuMap["stu02"]["name"] = "Mark"
stuMap["stu02"]["sex"] = "男"
stuMap["stu02"]["address"] = "北京"
for _, v1 := range stuMap {
for k2, v2 := range v1 {
fmt.Printf("k=%v, v=%v\\n", k2, v2)
}
}
}
案例演示相对复杂的 map 遍历, 该 map 的 value 又是一个 map
说明: map 的遍历使用 for - range 的结构遍历
map 的长度
len(map) 函数可以统计出 map 的函数
例子: \tfmt.Printf("map len=%v\n", len(cities))
map 切片
基本介绍
切片数据类型如果是 map , 则我们称为 slice of map, map 切片,这样则 map 个数就可以动态变化了
演示案例, 使用map 来 monster 的信息 name , age , 也就是说 monster 对应一个 map ,
并且 monster 个数可以动态增加的 => map 切片
测试代码案例
package main
import (
"fmt"
)
func main() {
monsters := make([]map[string]string, 10)
if monsters[0] == nil {
monsters[0] = make(map[string]string)
monsters[0]["name"] = "牛魔王"
monsters[0]["age"] = "500"
}
if monsters[1] == nil {
monsters[1] = make(map[string]string)
monsters[1]["name"] = "金角大王"
monsters[1]["age"] = "100"
}
fmt.Println(monsters)
}
append 函数使用
package main
import (
"fmt"
)
func main() {
monsters := make([]map[string]string, 10)
if monsters[0] == nil {
monsters[0] = make(map[string]string)
monsters[0]["name"] = "牛魔王"
monsters[0]["age"] = "500"
}
if monsters[1] == nil {
monsters[1] = make(map[string]string)
monsters[1]["name"] = "金角大王"
monsters[1]["age"] = "100"
}
//我们需要使用到切片的 append 函数,可以动态增加 monster
//1. 先创建一个 monster 信息
newMonsters := map[string]string {
"name" : "新的妖怪",
"age" : "200",
}
monsters = append(monsters, newMonsters)
fmt.Println(monsters)
}
map 排序
基本介绍
1. golang 中没有一个专门的方法针对 map 的key 进行排序
2. golang 中的 map 默认是无序的, 注意也不不是一个按照添加顺序的存放的 。每次便利,得到的输出结果是不一样的
3. golang 中的 map 排序, 先是对 key 进行排序,然后更具 key 值遍历输出即可
案例演示
package main
import (
"fmt"
"sort"
)
func main() {
map1 := make(map[int]int, 10)
map1[10] = 10
map1[1] = 10
map1[4] = 6
map1[8] = 16
fmt.Println(map1)
//如何按照 map 的key 的顺序进行排序输出
//1. 现将 map 的key 放入到一个切片中
//2. 对切片进行排序
//3. 遍历切片,然后按照 key 来输出 map 的值
var keys []int
for k, _ := range map1 {
keys = append(keys, k)
}
sort.Ints(keys)
fmt.Println(keys)
for _, v := range keys {
fmt.Printf("k=%v. v=%v\\n", v, map1[v])
}
}
map 的使用细节
1. map 是一个引用类型, 准守应用类型的传递机制, 在一个函数接受 map, 修改后,会直接修改原来 map
2. map 的容量达到后,如果在想忖度元素, 会自动拓容, 并不会发生 panic , 也就是说 map 能动态增减键值对 (key-value)
3. map 的 value 经常也是使用 struct 类型, 更适合管理复杂的数据, 比如 value 中是一个 Student 结构体
案例代码1
package main
import (
"fmt"
)
func main() {
map1 := make(map[int]int, 2)
map1[10] = 10
map1[1] = 10
map1[4] = 6
map1[8] = 16
modify(map1)
fmt.Println(map1)
//3. map 的 value 经常也是使用 struct 类型, 更适合管理复杂的数据, 比如 value 中是一个 Student 结构体
//1.map 的 key 是学号
//2.map 的 value 是学生的结构体
students := make(map[string]Student, 10)
students["00001"] = Student {
Name : "Tom",
Age : 100,
Sex : 1,
}
students["00002"] = Student {
Name : "Jack",
Age : 100,
Sex : 1,
}
//遍历学生信息
for k, v := range students {
fmt.Printf("stuNo=%v, stuName=%v\\n", k, v.Name)
}
//fmt.Println(students)
}
func modify(map1 map[int]int) {
map1[8] = 1600
}
type Student struct {
Name string
Age int
Sex byte
}
map 的练习
package main
import (
"fmt"
)
func modifyUser(users map[string]map[string]string, name string) {
if users[name] != nil {
users[name]["pwd"] = "888888"
} else {
users[name] = make(map[string]string, 2)
users[name]["pwd"] = "888888"
users[name]["nickname"] = "~~~ 昵称"
}
}
func main() {
users := make(map[string]map[string]string, 10)
users["Smith"] = make(map[string]string)
users["Smith"]["pwd"] = "999999"
users["Smith"]["nickname"] = "~~~ 昵称 Smith"
modifyUser(users, "Tom")
modifyUser(users, "Jack")
fmt.Println(users)
}
1. 使用 map[string]map[string]string 的 map 类型
2. key 表示用户名, 是唯一的不可以重复
3. 如果某个用户名存在, 就将其密码修改为 "888888", 如果补存在就增加这个用户信息, (包括昵称 nickname, 和密码 pwd)
4. 编写一个函数 modifyUser (users map[string]map[string]string, name string) 完成上述功能
golang 语言面向对象编程
1. goalng 也支持面向对象 (OOP), 但是和传统的面向对象编程有却别, 并不是纯粹的面向对象语言,
所以我们说 Golang 支持面向对象变成特征是比较准确的。
2. Golang 没有类 (class), Golang 语言的结构体(struct)和其他的语言类 (class)有同等的低位,
你可以理解 Golang 是基于 struct 来实现 oop 特征的。
3. Golang 面向对象编程非常简介, 去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏指针 this 等等
4. Golang 仍然有面向对象的继承,封装和多台的特征,只是实现方式和其他的 OOP 语言不一样,比如继承:
golang 没有 extends 关键字, 继承是通过匿名字段来实现的 。
5. Golang 面向对象 (OOP)很优雅, OOP 本身就是语言类型系统(type system) 的一部分, 通过接口(interface)关联,耦合性低,也非常灵活。后面充分会体现这个特征, 也就是说 Golang 中面向接口编程是非常重要的特征。
结构体的定义
声明结构体
type 标识符 struct {
field1 type
field2 type
}
举例子:
type Cat struct {
Name string,
Age int32,
}
字段/属性
1. 从概念上或叫法上看, 结构体字段 = 属性 = field
2. 字段是结构体的一个重要注册和那个部分, 一般是基本数据类型、数组、也可以是引用类型。
比如我们之前定义猫结构体的 Name string 就是属性
注意细节说明
1. 字段语法同变量:示例: 字段名 字段类型
2. 字段的类型可以分为:基本类型,数组,引用类型
3. 在创建一个结构体变量后,如果没有给字段赋值,都是对应一个 零值 (默认值), 规则同前面的一样:
布尔类型是 false , 数值是 0 ,字符串是 ""
数组类型的默认值和它的元素类型相关,比如 score[3] int 则为 [0,0,0]
指针, slice , 和 map 的零值都是 nil 即还没有分配空间
细节说明代码
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
Scores [5]float64
ptr *int
slice []int
map1 map[string]string
}
func main() {
var p1 Person
if p1.ptr == nil {
fmt.Println("p1.ptr is nil")
}
p1.slice = make([]int, 10)
fmt.Println(p1)
}
4. 不同结构体变量的字段都是独立的,互不影响, 一个结构体字段的更改不影响另外一个。(结构体是值类型)
细节说明代码
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
Scores [5]float64
ptr *int
slice []int
map1 map[string]string
}
type Monster struct {
Name string
Age int
}
func main() {
var p1 Person
if p1.ptr == nil {
fmt.Println("p1.ptr is nil")
}
p1.slice = make([]int, 10)
fmt.Println(p1)
//不同的结构体变量是相互独立的, 互不影响,结构体变量字段的更改
//不影响另外一个结构体的值
var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 100
monster2 := monster1
monster2.Name = "孙悟空"
fmt.Println("monster1=", monster1)
fmt.Println("monster2=", monster2)
//输出结果
//monster1= {牛魔王 100}
//monster2= {孙悟空 100}
}
创建结构体变量和访问结构体字段
1. 方式 1- 直接声明
var person Person
2. 方式 2 - {}
var person Person = Person{}
3. 方式3 &
var person *Person = new(Person)
4. 方式 4 {}
var person *Person = &Person{}
1. 第三种和第四种返回的是 结构体指针
2. 结构体指针访问字段的标准方式是: (结构体指针). 字段名, 比如 (persion).Name = "tom"
3. 但 go 做了简化, 也支持结构体指针.字段名,比如 : person.Name = "tom" 更加符合程序员的使用习惯,
go 编译器底层堆 person.Name 做了转化 (*person).Name
struct 类型的内存分配机制
基本说明
变量总是在内存中的
实例代码
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
}
func main() {
var p1 Person
p1.Name = "小明"
p1.Age = 20
var p2 *Person = &p1
fmt.Println((*p2).Age)
fmt.Println(p2.Age)
p2.Name = "小王、、"
fmt.Printf("p2.Name = %v, p1.Name =%v \\n", p2.Name, p1.Name)
fmt.Printf("p2.Age = %v, p1.Age =%v \\n", p2.Age, p1.Age)
fmt.Printf("p1 的地址 %p \\n", &p1)
fmt.Printf("p2 的地址 %p p2值的地址 %p\\n", &p2, p2)
//输出结果
// 20
// 20
// p2.Name = 小王、、, p1.Name =小王、、
// p2.Age = 20, p1.Age =20
// p1 的地址 0xc00008e020
// p2 的地址 0xc00009a018 p2值的地址 0xc00008e020
}
\tvar p1 Person
\tp1.Name = "小明"
\tp1.Age = 20
\tvar p2 Person = &p1
\t//不能这样写,编译报错,点的运算优先级比 * 的高
\tfmt.Println(p2.Age)
结构体的注意事项和使用细节
1. 结构体的所有字段的内存地址是连续分布的 。
证明代码
package main
import(
"fmt"
)
//结构体
type Point struct {
x int
y int
}
//结构体
type Rect struct {
leftUp, rightDown Point
}
//结构体2
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect{
Point{1, 2}, Point{3, 4},
}
//r1 结构体中有 4 个整数是连续分布的
fmt.Printf("r1 的地址 %p\n", &r1)
fmt.Printf("r1.leftUp 的地址 %p\n", &r1.leftUp)
fmt.Printf("r1.rightDown 的地址 %p\n", &r1.rightDown)
fmt.Printf("r1.leftUp.x 的地址 %p, \nr1.leftUp.y 的地址 %p, \nr1.rightDown.x 的地址 %p, \nr1.rightDown.y 的地址 %p, \n",
&r1.leftUp.x,
&r1.leftUp.y,
&r1.rightDown.x,
&r1.rightDown.y,
)
//r2 有两个 *Point 类型,这两个 *Point 类型的本身地址也是连续的, 但是他们指向的地址不一定是连续的
r2 := Rect2{
&Point{1, 2}, &Point{3, 4},
}
fmt.Println()
//打印地址
fmt.Printf("r2.leftUp 的地址 %p, r2.rightDown 的地址 %p\n",
&r2.leftUp,
&r2.rightDown,
)
//他们指向的地址不一定是连续的。。。 这个需要看内存运行时的情况
fmt.Printf("r2.leftUp 指向地址 %p, r2.rightDown 指向地址 %p\n",
r2.leftUp,
r2.rightDown,
)
}
//运行输出
// r1 的地址 0xc0000aa000
// r1.leftUp 的地址 0xc0000aa000
// r1.rightDown 的地址 0xc0000aa010
// r1.leftUp.x 的地址 0xc0000aa000,
// r1.leftUp.y 的地址 0xc0000aa008,
// r1.rightDown.x 的地址 0xc0000aa010,
// r1.rightDown.y 的地址 0xc0000aa018,
// r2.leftUp 的地址 0xc0000841e0, r2.rightDown 的地址 0xc0000841e8
// r2.leftUp 指向地址 0xc0000a4020, r2.rightDown 指向地址 0xc0000a4030
2. 结构体是用户单独定义的类型, 和其他类型进行转化的时候需要完全相同的字段名(名字、个数和类型)
实践代码
package main
import(
"fmt"
)
//结构体
type A struct {
Num int
}
//结构体
type B struct {
Num int
}
func main() {
var a A
var b B
b.Num = 100
a = A(b) //可以转换,结构体的字段要完全一样,包括名字,个数,字段类型
fmt.Println(a, b)
}
3. 结构体进行 type 重新定义(相当于取别名), Golang 认为是行的数据类型, 但是可以互相转换
4. struct 的每个字段上,可以写一个 tag , 该tag, 可以通过反射机制获取,常见的使用场景是序列化和反序列化。
序列化的场景
1. JSON转换
package main
import(
"fmt"
"encoding/json"
)
//结构体
type A struct {
Num int
}
//结构体
type B struct {
Num int
}
//结构体
type Monster struct {
Name string `json:"name"`
Age int `json:"aget"`
Skill string `json:"skill"`
}
func main() {
var a A
var b B
b.Num = 100
a = A(b) //可以转换,结构体的字段要完全一样,包括名字,个数,字段类型
fmt.Println(a, b)
//1.先创建一个 Monster 变量
monster := Monster{"牛魔王", 500, "芭蕉扇"}
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("序列化 JSON 出错", err)
}
fmt.Println("jsonStr", string(jsonStr))
}
基本介绍
在某些情况下,我们需要申明(定义)方法。比如 Person 结构体,除了一些字段外(年龄,姓名。。。。)Person 结构体还有一些行为,比如跑步,通过学习, 还可以做算数题。这就要用到方法才能完成
Golang 中的方法是指作用在执行类型上的(即, 和制定的数据类型绑定), 因此自定义类型,都可以有方法,不仅仅是 struct
方法的申明和调用
type A struct {
Num int
}
func (a A) test () {
fmt.Println(a.Num)
}
举例说明
package main
import (
"fmt"
)
type Person struct {
Name string
}
func (p Person) test() {
fmt.Println(p.Name)
}
func main() {
var p Person = Person {"张三"}
p.test();
}
1. test 方法是 Person 类型的板顶
2. test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其他类型来调用
3. func (p Person)test () {。。。} 表示那个 Person 变量调用, 这个 p 就是它的副本, 这点和函数传递非常相似
4 。 p 这个变量名是程序员自己指定的, 而不是固定的, 比如修改为 person 也是可以的。
方法的快速入门
1. 给 Person 结构体添加 speak 方法输出, xxx 是一个好人
2. 给 Peson 结构体添加计算方法 , 可以计算 从 1+ ... 100 的结果
3. 给 Peson 结构体 jisuan2 , 该方法可以接受一个数n, 计算 1 + .... n 的结果
4. 给 Person 结构体添加 getSum 方法,可以计算两个数的和, 并且返回结果
方法调用的传参机制和原理
说明:方法调用和传递参数机制和函数基本一样,不一样的地方是, 会讲调用方法的变量,也就是实参传递给方法
1. 通过一个变量去调用方法的时候, 其调用机制和函数一样
2. 不一样的地方, 变量调用方法时, 该变量本省也会作为一个参数传递到方法(如果变量是值类型的, 则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
方法的申明
func (recevier type) methodName (参数列表) (返回值列表) {
方法体
return 返回值
}
1. 参数列表,表示方法输入
2. recevier type :表示这个方法和 type 的这个类型板顶, 或者说是该方法作用于 type 类型
3.recevier type: type 可以是结构体,也可以是其他的自定义类型
4. receiver:就是 type 类型的一个变量(实例),比如:Person 结构体的一个变量(实例)
5.参数列表,表示方法输入
6.返回值列表, 表示返回值, 可以多个
7. 方法主体, 表示为了实现某个功能代码块
8. return 语句不是必须的
方法注意事项和细节讨论
1.结构体类型是值类型, 在方法调用中,遵循值类型的传递机制 是值拷贝传递方式
2.如果程序员希望在方法中, 修改结构体变量的值, 可以通过结构体指针的方式来处理
3.Golang 的方法作用在指定的数据类型上(即:和制定的数据类型板顶),因此自定义类型,都可以有方法,而不仅仅是 struct ,比如 int, float32 都可以
4. 方法的访问, 范围控制规则和函数一样, 方法名首字母小写, 只能本包内访问,方法首字母大写, 可以在本包和其他包访问
5. 如果一个变量实现了 String() 方法, 那么 fmt.Println 会默认调动这个便离开那个的 String() 进行输出
代码实例
package main
import (
"fmt"
)
type integer int
func (i *integer) print() {
fmt.Println("i=", *i)
}
func (i *integer) change() {
*i += 1
}
type Student struct {
Name string
Age int
}
func (stu *Student) String() string {
str := fmt.Sprintf("Name=[%v], Age=[%v]\\n", stu.Name, stu.Age)
return str
}
func main() {
var i integer = 10
i.change()
i.print()
//定义个 Student 变量
stu := Student {
Name : "Tom",
Age : 40,
}
//如果你实现了 *Student 的 String() 方法,那么就会自动调用
fmt.Println(&stu)
}
方法和函数的却别
调用方式不一样 : 函数调用方式通过 函数名(实参列表),方法调用方式:变量.方法名(实参列表)
对于与普通函数,接受者为值类型,不能将指针类型的数据直接传递, 反之亦然
对于方法, 如struct 的方法, 接受者为值类型,可以通过指针变量调用方法, 反过来也可以
创建结构体变量时指定字段的值
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
}
func main() {
//
var stu1 Student = Student{"小明", 23}
stu2 := Student{"小明", 23}
//在创建结构体变量的时候字段名和字段值写在一起
var stu3 = Student {
Name : "jack",
Age : 20,
}
fmt.Println(stu1)
fmt.Println(stu2)
fmt.Println(stu3)
//方式二 ,返回结构体的指针类型
var stu = &Student {
Name : "小王",
Age : 20,
}
fmt.Println(stu)
}
Golang 在创建结构体实例(变量)时, 可以直接指定字段的值
方式一
var stu1 Student = Student {"tom", 10}
方式二
var stu *Student = &Student{
Name : "tim",
Age : 20
}
常用的文件操作的函数和方法
1. 打开一个文件进行读操作
os.Open(name string) (*File, err)
2. 关闭一个文件 File.Close()
3. 其他的函数和方法在案例中讲解
对文件的简单操作
package main
import (
"fmt"
"os"
)
func main() {
//打开一个文件
//概念说明:
//1. file 叫 file 对象
//2. file 叫 file 指针
//3. file 叫 file 文件句柄
file, err := os.Open("/home/lxl/Pictures/file.png")
if err != nil {
fmt.Println("open file err=", err)
}
fmt.Printf("file=%v\\n", file)
//关闭文件
err = file.Close()
if err != nil {
fmt.Println("关闭文件错误")
}
}
文件读取实例
1. 读取文件内容并在终端(带缓冲的方式),使用 os.Open , file.Close . bufio.NewReader(), reader.ReadString 函数和方法
使用带缓冲区的方式来读取文件
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
//打开一个文件
//概念说明:
//1. file 叫 file 对象
//2. file 叫 file 指针
//3. file 叫 file 文件句柄
file, err := os.Open("/home/lxl/Pictures/test.txt")
if err != nil {
fmt.Println("open file err=", err)
}
fmt.Printf("file=%v\\n", file)
//关闭文件, 要及时关闭file
defer file.Close() //要即使关闭 file 句柄, 否则会有内存泄露的风险
//创建一个 *Reader, 是带缓冲区
reader := bufio.NewReader(file)
//循环的读取文件的内容
for {
s, err := reader.ReadString(\'\\n\') //读取到换行的时候就结束
if err == io.EOF { //io.EOF 表示文件末尾
break
}
//输出内存
fmt.Println(s)
}
fmt.Println("文件读取结束...")
}
实践代码
package main
import (
"fmt"
"io/ioutil"
)
func main() {
//使用ioutil.ReadFile一次性讲文件读取到位
p := "/home/lxl/Pictures/test.txt"
context, err := ioutil.ReadFile(p)
if err != nil {
fmt.Println("open file err=", err)
}
//把读取到内容显示到终端
fmt.Printf("%v\\n", string(context)) //byte[]
//我们没有显式的 Open 文件, 因此也不需要显式的 close 文件
//因为,文件的 Open 和 Close 被封装到 ReadFile 函数内部
}
写文件操作
基本介绍
func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
说明: os.OpenFile 是一个更一般性的文件打开函数, 它会使用制定的选项(如:O_RDONLY 等)、
指定的模式(如0666 等)打开制定名称的文件。如果操作成功呢 返回的文件对象可用于 I/O , 如果出错错误底层类型是 *PathError
第二个参数:文件打开模式(可以组合)
第三个参数:权限控制 liunx
r -> 4
w -> 2
x -> 1
基本应用案例-方式一
1. 创建一个新的文件, 写入内容 5 句 "hello, Gardon"
实现代码
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//创建一个文新文件,写入内容, 5 句话 "hello world"
//1.打开文件 /home/lxl/Pictures/test.txt
filePath := "/home/lxl/Pictures/test.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\\n", err)
}
//及时关闭file句柄防止内存泄漏
defer file.Close()
//准备写入 hello Gardon
str := "hello Gardon\\n"
//写入时采用带缓冲的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
//应为 writer 是带缓存的,其实内容并没有被写入到文件,需要掉用 Flush 方法将缓存的数据真正写入到文件中
writer.Flush()
}
2. 打开一个存在的文件中,讲原来的内容覆盖成新的内容 10 句 "你好, 张三"
3. 打开一个存在的文件, 在原来的内容追加内容 "ABCI ENGLISHI"
4. 打开一个存在的文件,将原进来的内容读取显示到终端商, 并且追加 5 句 "hello 北京"
使用 os.OpenFile(), bufio.NewWriter(), * Writer 的方法 WriteString 完成上面的任务
方式二
代码实现
package main
import (
"fmt"
"io/ioutil"
)
func main() {
//1. 首先将 /home/lxl/Pictures/test.txt 内容读取到内存
//2. 将文件的内容写入到 /home/lxl/Pictures/kkk.txt 文件
file1Path := "/home/lxl/Pictures/test.txt"
file2Path := "/home/lxl/Pictures/kkk.txt"
data, err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Printf("read file err %v \\n", err)
return
}
err = ioutil.WriteFile(file2Path, data, 666)
if err != nil {
fmt.Printf("write file err %v \\n", err)
}
}
判断文件或者文件夹是否存在
golang 判断文件或文件夹是否存在的方法使用 os.Stat() 函数返回值的错误进行判断:
1. 如果返回错误为 nil, 说明文件或者文件夹存在
2. 如果返回的错误类型使用 os.IsNotExist() 判断为 true , 说明文件或者文件夹不存在
3. 如果返回的错误为其他类型, 则不确定是否存在
实现代码:
func PathExists(path string) (bool, error) {
\t_, err := os.Stat(path)
\tif err != nil {
\t\treturn true, nil
\t}
\tif os.IsNotExist(err) {
\t\treturn false, nil
\t}
\treturn false, err
}
拷贝文件
将一张图片/电影/mp3 拷贝到另外一个文件 e:/abc.jpg io 包 func Copy (dst Writer, src Reader) (written int64, err error)
实现代码
package main
import (
"fmt"
"os"
"bufio"
"io"
)
func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {
srcFile, err := os.Open(srcFileName)
if err != nil {
fmt.Printf("open file err %v \\n", err)
}
defer srcFile.Close()
//通过srcFile 获取到 Reader
reader := bufio.NewReader(srcFile)
//打开 dstFileName
dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY| os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v", err)
return
}
defer dstFile.Close()
//通过dstfile, 获取到 writer
writer := bufio.NewWriter(dstFile)
return io.Copy(writer, reader)
}
func main() {
// 将 ~/Pictures/file.png 文件拷贝到 ~/Downloads/file.png
srcFile := "/home/lxl/Pictures/file.png"
dstFile := "/home/lxl/Downloads/file.png"
_, err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Println("拷贝完成")
} else {
fmt.Printf("拷贝出错 err %v \\n", err)
}
}
统计英文, 数字,空格,和其他字符数量
实现代码
package main
import (
"fmt"
"os"
"bufio"
"io"
)
//定义一个结构体用于保存统计的结果
type CharCount struct {
chCount int // 记录英文个数
NumCount int // 记录数字的个数
SpaceCount int // 记录空格的个数
OtherCount int // 记录其他字符个数
}
//统计英文, 数字,空格,和其他字符数量
func main() {
//打开一个文件,创建一个Reader
//读取一行,就取统计该行有多少个英文, 数字, 空格和其他字符
//然后保存到一个结构体中
fileName := "abc.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("open file err %v \\n", err)
return
}
defer file.Close()
//定义一个CharCount 实例
var cc CharCount
//创建一个reader
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString(\'\\n\')
if err == io.EOF { //读取到文件末尾就退出
break
}
//遍历 str
for _, v := range str {
//fmt.Println(v)
switch {
case v >= \'a\' && v <= \'z\':
fallthrough //穿透
case v >= \'A\' && v <= \'Z\':
cc.chCount++
case v == \' \' || v == \'\\t\':
cc.SpaceCount++
case v >= \'0\' && v <= \'9\':
cc.NumCount++
default:
cc.OtherCount++
}
}
}
fmt.Printf("字符个数 %v 数字的个数 %v 空格的个数 %v 其他字符个数 %v \\n",
cc.chCount, cc.NumCount, cc.SpaceCount, cc.OtherCount)
}
命令行参数
我们希望获取到命令行输入的各种参数, 命令行参数
基本介绍
os.Args 是一个string 切片, 用来存储所有的命令行参数
示例代码
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("命令行的所有参数", len(os.Args))
//遍历 os.Args 欺骗, 就可以得到所有的命令行输入参数值
for i, v := range os.Args {
fmt.Printf("args[%v]=%v\\n", i, v)
}
}
flag 包来解析命令行参数
说明:前面的方式是比较原生的方式, 对解析参数不是特别的方便, 特别是带有指定参数形式的命令行
比如:cmd> main.exe -f c:/aa.txt -p 200 -u root 这样的命令行, go 设计者给我们提供了 flag 包,
可以方便的解析命令行参数,而且参数顺序可以随意
代码示例
package main
import (
"fmt"
"flag"
)
func main() {
//定义几个变量,用于接受命令行参数
var user string
var password string
var host string
var port int
flag.StringVar(&user, "u", "", "用户名, 默认为空")
flag.StringVar(&password, "p", "", "密码, 默认为空")
flag.StringVar(&host, "h", "localhost", "主机地址, 默认为localhost")
flag.IntVar(&port, "port", 80, "主机端口, 默认为80")
//这里有一个非常重要的操作, 转换, 必须调用该方法
flag.Parse()
fmt.Printf("用户名 %v 密码 %v 主机地址 %v 端口 %v \\n", user, password, host, port)
}
调用
go run main.go -u root -p zshhda -h 127.0.0.1 -port 100000
JSON 数据格式
JSON 的序列化
化序列案例
package main
import (
"encoding/json"
"fmt"
)
//定义一个结构体
type Monster struct {
Name string
Age int
Birthday string
Sal float64
SKill string
}
func testStruct() {
//演示
monster := Monster{
Name: "XXX",
Age: 500,
Birthday: "2009-01-01",
Sal: 1000,
SKill: "AAA",
}
bytes, err := json.Marshal(&monster)
if err != nil {
fmt.Printf("序列化失败,err %v \\n", err)
} else {
fmt.Printf("序列化成功,序列化的结果是 %v \\n", string(bytes))
}
//输出结果:
//序列化成功,序列化的结果是 {"Name":"XXX","Age":500,"Birthday":"2009-01-01","Sal":1000,"SKill":"AAA"}
}
//演示 map 的序列化
func testMap() {
var a map[string]interface{}
//使用map 之前需要make
a = make(map[string]interface{}, 10)
a["name"] = "红孩儿"
a["age"] = 200
bytes, err := json.Marshal(&a)
if err != nil {
fmt.Printf("序列化失败,err %v \\n", err)
} else {
fmt.Printf("序列化成功,a map 序列化的结果是 %v \\n", string(bytes))
}
//输出结果:
//序列化成功,a map 序列化的结果是 {"age":200,"name":"红孩儿"}
}
//演示切片的序列化,定义切片 []map[string]interface{}
func testSlice() {
var sli []map[string]interface{}
m1 := make(map[string]interface{}, 10)
m1["name"] = "张三"
m1["age"] = 20
m1["address"] = "北京"
m2 := make(map[string]interface{}, 10)
m2["name"] = "李四"
m2["age"] = 20
m2["address"] = [2]interface{}{"上海", "夏威夷"}
sli = append(sli, m1)
sli = append(sli, m2)
//切片进行序列化
bytes, err := json.Marshal(&sli)
if err != nil {
fmt.Printf("序列化失败,err %v \\n", err)
} else {
fmt.Printf("序列化成功,切片序列化的结果是 %v \\n", string(bytes))
}
//输出结果:
//序列化成功,切片序列化的结果是 [{"address":"北京","age":20,"name":"张三"},{"address":"上海","age":20,"name":"李四"}]
}
//基本数据类型序列化
func testFloat64() {
var num1 float64 = 2.12121
//基本数据类型序列化
bytes, err := json.Marshal(&num1)
if err != nil {
fmt.Printf("序列化失败,err %v \\n", err)
} else {
fmt.Printf("序列化成功,基本数据类型序列化的结果是 %v \\n", string(bytes))
}
//输出结果:
//序列化成功,基本数据类型序列化的结果是 2.12121
}
func main() {
testSlice()
testFloat64()
}
json tag
案例:
json:"name"json:"age"json:"birthday"json:"sal"json:"skill"
输出 json :
{"name":"XXX","age":500,"birthday":"2009-01-01","sal":1000,"skill":"AAA"}
JSON 反序列化
介绍: json 反序列化是指, 将 json 字符串反序列化成对应的数据类型(比如结构体, map, 切片)的操作
应用案例:我们这里介绍字符串反序列化, 成结构体, map 和 切片
测试代码
package main
import (
"encoding/json"
"fmt"
)
//定义一个结构体
type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Birthday string `json:"birthday"`
Sal float64 `json:"sal"`
SKill string `json:"skill"`
}
//演示将 JSON 字符串反序列化为结构体
func unmarshalStruct() {
str := "{\\"name\\":\\"XXX\\",\\"age\\":500,\\"birthday\\":\\"2009-01-01\\",\\"sal\\":1000,\\"skill\\":\\"AAA\\"}"
var monster Monster
err := json.Unmarshal([]byte(str), &monster)
if err != nil {
fmt.Printf("反序列化失败,错误信息 err %v \\n", err)
} else {
fmt.Printf("反序列化成功,输出信息: %v \\n", monster)
}
}
func unmarshalMap() {
str := "{\\"name\\":\\"XXX\\",\\"age\\":500,\\"birthday\\":\\"2009-01-01\\",\\"sal\\":1000,\\"skill\\":\\"AAA\\"}"
var m map[string]interface{} //如果反序列化 map 不需要make, make 封装到了 Unmarshal
err := json.Unmarshal([]byte(str), &m)
if err != nil {
fmt.Printf("反序列化失败,错误信息 err %v \\n", err)
} else {
fmt.Printf("反序列化成功,map 输出信息: %v \\n", m)
}
}
func unmarshalSlice() {
str := "[{\\"name\\":\\"XXX\\",\\"age\\":500,\\"birthday\\":\\"2009-01-01\\",\\"sal\\":1000,\\"skill\\":\\"AAA\\"}]"
var m []Monster //如果反序列化 map 不需要make
err := json.Unmarshal([]byte(str), &m)
if err != nil {
fmt.Printf("反序列化失败,错误信息 err %v \\n", err)
} else {
fmt.Printf("反序列化成功,slice 输出信息: %v \\n", m)
}
}
func main() {
unmarshalStruct()
unmarshalMap()
unmarshalSlice()
//输出结果
//反序列化成功,输出信息: {XXX 500 2009-01-01 1000 AAA}
//反序列化成功,map 输出信息: map[age:500 birthday:2009-01-01 name:XXX sal:1000 skill:AAA]
//反序列化成功,slice 输出信息: [{XXX 500 2009-01-01 1000 AAA}]
}
小结说明
1. 在反序列化一个字符串的时候,确保反序列化的数据类型和反序列化前的数据类型一致
2. 如果字符串通过程序获取到的, 则不需要对 "" 转义处理
Go 协程和 Go 主线程
1. Go 主线程 (有人直接称为线程/也可以理解为进程);一个Go线程上, 可以开启多个协程,可以这样理解协程就是一个轻量级的线程
2. Go 协程的特点
1). 有独立的栈空间
2) 共享线程堆空间
3)调度由用户控制
4)协程是轻量级的线程
channel (管道)
不同 goroutine 之间如何通讯
2. channel
为什么需要 channel
前面降到使用去全局变量加锁同步来解决 goroutine 的通讯, 但并不完美
1. 主线程等待所有的 goroutine 全部完成的时间很难去而定,我们这里设置的是 10秒, 但是仅仅是估算
2. 如果主线程休眠的时间长了, 会加长等待时间,如果等待时间断了,还有可能 goroutine 处于工作状态, 这时也会随着主线程的退出而消亡
3. 通过全局变量来加锁实现通讯, 也并不利用多个协程对全局变量的读写操作
4. 上面的分析, 都在为了引出一个新的通讯机制 channel
channel 的基本介绍
1. channel 的本质是一个数据结构 - 队列
2. 数据是先进先出的 【FIFO】
3. 线程安全, 多 goroutine 访问时, 不需要加锁, 就是说 channel 本身就是线程安全的
4. channel 是有类型的, 一个 string 的 channel 只能存放 string 数据类型
示意图
channel 的基本使用
channel 初始化
使用make 进行初始化
var intChan chan int
intChan = make(chan int, 10)
向 channel 写入(存放)数据
var intChan chan int
intChan =make(chan int , 10)
num := 999
intChan <- 10
intChan <- num
简单使用代码
package main
import "fmt"
func main() {
//演示个管道的使用
var intChan chan int
//1,创建一个能够存放 3 个 int 的 chan
intChan = make(chan int, 3)
//2.看看intChan 是什么
fmt.Printf("intChan 的值是 %v, intChan 本身的地址是 =%p \\n", intChan, intChan)
//3.向管道写入数据
intChan <- 10
num := 999
intChan <- num
intChan <- num
// intChan <- 98 注意点: 我们给管道写入数据的时候不能超过其容量
//4.输出管道的长度和 cap(容量)
fmt.Printf("channel len=%v, cap=%v\\n", len(intChan), cap(intChan))
//5.从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Printf("num2 = %v\\n", num2)
fmt.Printf("channel len=%v, cap=%v\\n", len(intChan), cap(intChan))
//6.在没有使用协程的情况下如果我们的管道已经全部取出,再取出就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
fmt.Printf("num3 = %v num4 = %v\\n", num3, num4)
}
channel 的使用注意事项
1. channel 中只能存放指定的数据类型
2. channel 的数据放满后, 就不能再放了
3. 如果 cahnnel 取出数据后,可以继续放了
4. 没有使用协程的情况下,如果 channel 数据取完了, 再取出就会报 dead lock
读写 channel 的案例
1. 创建一个 intChan 最多可以窜访 3 个 int , 演示存 3 个数据到 intChan , 然后在取出这三个 int
实现代码
package main
import "fmt"
func main() {
//演示个管道的使用
var intChan chan int
//1,创建一个能够存放 3 个 int 的 chan
intChan = make(chan int, 3)
intChan <- 10
intChan <- 20
intChan <- 30
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
fmt.Printf("num1 = %v num2 = %v num3 = %v\\n", num1, num2, num3)
}
2. 创建一个 mapChan , 最多可以存放 10 个 map[string]string 的key-val , 演示数据读取
3. 创建一个 catChan , 最多可以存放 10个 Cat 结构体变量,演示写入和读取的方法
4. 创建一个 catChan , 最多可以存放 10个*Cat 结构体变量,演示写入和读取的方法
5.创建一个 allChan , 最多可存放 10 个任意类型变量, 演示写入和读取的方法
6. 看看代码会输出什么?
func main() {
var allChan chan interface {}
allChan = make(chan interface{}. 10 )
cat1 := Cat{"tom", 2}
cat1 := Cat{"jack..", 1}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
cat11 := <- allChan
fmt.Println(cat11.Name)
}
更正后的代码
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{"tom", 2}
cat2 := Cat{"jack..", 1}
allChan <- 10
allChan <- "jack"
allChan <- cat1
allChan <- cat2
//我们需要获取管道中的第三个元素,那么这需要先推掉前面两个数据
<-allChan
<-allChan
newCat := <-allChan
a := newCat.(Cat)
fmt.Printf("newCat=%T, newCat=%v\\n", newCat, newCat)
fmt.Printf("newCat.Name=%v\\n", a.Name)
//输出结果:
// newCat=main.Cat, newCat={tom 2}
// newCat.Name=tom
}
完成如下练习
1. 创建一个 Person 结构体 {Name, Age , Address}
2. 使用 rand 方法随机创建10 个Person 实例, 并放入到 channel 中
3. 遍历channel, 将各个 Person 实例在终端上显示
channel 的遍历和关闭
channel 的关闭
使用内置函数 close 可以关闭 channel , 当关闭 channel 之后,就不能再向 channel 写数据了,
但是仍然可以从该 channel 中读取数据
关闭演示代码
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main() {
//演示个管道的使用
var intChan chan int
//1,创建一个能够存放 3 个 int 的 chan
intChan = make(chan int, 3)
intChan <- 10
intChan <- 20
close(intChan)
// 错误 intChan <- 30 panic: send on closed channel
num1 := <-intChan
num2 := <-intChan
// num3 := <-intChan
fmt.Printf("num1 = %v num2 = %v \\n", num1, num2)
}
channel 的遍历
channel 支持 for - range 的方式进行遍历, 注意两个细节
1. 在遍历时, 如果 channel 没有关闭, 则会出现 deadlock 的错误
2. 在遍历时, 如果 channel 已经关闭, 则会正常遍历数据, 遍历完成后,还会退出遍历
管道遍历
//遍历管道
\tintChan2 := make(chan int, 100)
\tfor i := 0; i <= 100; i++ {
\t\tintChan2 <- i * 2
\t}
\t//这里管道没有关闭出现死锁, 提示:fatal error: all goroutines are asleep - deadlock!
\tclose(intChan2) //关闭管道
\tfor v := range intChan2 {
\t\tfmt.Printf("v=%v\n", v)
\t}
channel 遍历和关闭的演示
channel 遍历和关闭小结
1. 前面的案例演示了对管道数据进行遍历, 就是等价于从管道中取出数据即: <- ch]
2. 注意要 close 管道, 否则会出现 deadlock
3. 在 for range 管道时, 当遍历到最后的时候,发现管道关闭了, 就结束从管道读取数据的遍历工作,正常退出
4. 在 for range 管道时, 当遍历到最后的时候, 发现没有没关闭, 程序会认为可能有数据写入, 因此会等待,如果程序没有写入数据,则就会死锁
应用案例
应用案例1
完成 goroutine 和 channel 协同工作的案例 , 具体要求
1. 开启一个 writeData 协程 , 向管道 intChan 中写入 50 个整数
2. 开启一个 readData 协程, 从管道 intChan 中读取 writeData 写入数据
注意: writeData 和 readData 的操作都是同一个管道
3. 主线程需要 writeData 和 readData 协程都完成工作才能退出
案例代码
package main
import (
"fmt"
"time"
)
func main() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go readData(intChan, exitChan)
go writeData(intChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Printf("readData 读取到数据 %v\\n", v)
}
//readData 任务完成
exitChan <- true
close(exitChan)
}
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Printf("writeData 写入数据 %v\\n", i)
time.Sleep(100 * time.Millisecond)
}
close(intChan)
}
案例图示
作业1(goroutine 练习)
1. 启动一个协程, 讲 1-2000 的数放入到一个channel 中, 比如 numChan
2. 启动 8 个协程, 从 numChan 中取出 (比如 n)并且计算 1 + 2 + 。。 n 的和, 并且存放到 resChan
3. 最后 8 个协同完成工作后 ,再遍历 resChan , 显示结果 如 res[i] = 1... res[10] = 55 ..
4. 注意:考虑 resChan chan int 师傅合适?
作业2(goroutine + channel 配合完成排序, 并写入文件)
1. 开启一个协程 writeDataToFile , 随机生成 1000 个数据, 存放到文件中
2. 当 writeDataToFile 完成 1000 个数据写到文件后, 让 sort 协程从文件中读取 1000 个文件
, 并且完成排序, 重写写入到另外一个文件
3.考察点:协程和管道 + 文件的综合使用
4. 功能拓展:开 10 个协程 writeDataToFile , 每个协程随机生成 1000 个数据, 存入到 10个文件中
5. 当10 个文件都生成了, 让 10 个 sort 协程从10 个文件中读取 1000 个数据, 并且完成排序,重新写入 10 个文件中
应用实例2 阻塞
实验案例
\t//创建两个管道
\tintChan := make(chan int, 10) // 10 -> 50 的化数据一下就放入了
\texitChan := make(chan bool, 1)
\tgo readData(intChan, exitChan)
\tgo writeData(intChan)
\tfor {
\t\t_, ok := <-exitChan
\t\tif !ok {
break
\t\t}
\t}
问题: 如果注释掉 go readData(intChan, exitChan) 会怎么样
答:如果只是向管道中写入数据, 而没有读取(如果, 编译器(运行)发现了一个管道只有写, 而没有读取, 则该管道, 会阻塞。写管道和读管道的频率不一致, 无所谓), 就会出现阻塞而导致 dead lock , 原因是 intChan 容量是 10 , 而代码writeData 会写入 50 个数据,因此会阻塞在 writeData ch <- i
演示完整代码
package main
import (
"fmt"
"time"
)
func main() {
//创建两个管道
intChan := make(chan int, 10)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
time.Sleep(200 * time.Millisecond)
fmt.Printf("readData 读取到数据 %v\\n", v)
}
//readData 任务完成
exitChan <- true
close(exitChan)
}
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Printf("writeData 写入数据 %v\\n", i)
time.Sleep(100 * time.Millisecond)
}
close(intChan)
}
3. 应用案例
需求:要求统计 1-2000000 的数字中, 那些是素数?这个问题在开篇就提出了,我们现在有了 goroutine 和 channel 的知识后, 就可以完成了
分析思路
1. 传统的方法, 使用一个循环, 循环的判断各个数是不是素数
2. 使用并发/并行的方式, 将统计素数的任务分配给(4个)goroutine 取完成,完成任务时间段
分析思路
实现代码
package main
import (
"fmt"
//"time"
)
func putNum(intChan chan int) {
for i := 1; i <= 1000; i++ {
intChan <- i
}
//关闭
close(intChan)
}
func priceNum(intChan chan int, primeChan chan int, exitChan chan bool) {
for {
//time.Sleep(10 * time.Millisecond)
//判断num是不是素数
flag := true
num, ok := <-intChan
if !ok {
break
}
for i := 2; i < num; i++ {
if num%i == 0 { //说明该num不是素数
flag = false
break
}
}
if flag {
//如果是素数,将这个数放入 primeChan
primeChan <- num
}
}
fmt.Printf("有一个 primeNum 协程因为取不到数据,退出\\n")
//这里不能关闭 primeChan
//退出管道写入一个标志位
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000) //放入结果
exitChan := make(chan bool, 4) //4个
//开启一个协程,向intChan 翻入 1-8000 个数
go putNum(intChan)
//开启4个协程, 从 intChan 取出数据,并且判断是否为素数, 如果是,就4
//放入到 primeChan
for i := 0; i < 4; i++ {
priceNum(intChan, primeChan, exitChan)
}
//主线程进行处理
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
//当我们从 exitChan 取出了 4个结果我们就可以放心的关闭 primeChan
close(primeChan)
}()
//遍历primeChan, 把结果取出
for {
res, ok := <-primeChan
if !ok {
break
}
fmt.Printf("素数=%d \\n", res)
}
fmt.Printf("主线程退出")
close(exitChan)
}
说明:使用 goroutine 完成后, 可以使用传统的方式来统计下看看这完成这个任务, 各自耗费的时间是多少, [用 map 保存 primeNum]
普通方法和协程方法时间统计
普通方法
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now().Unix()
for num := 1; num <= 80000; num++ {
flag := true
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
//如果素数
}
}
end := time.Now().Unix()
fmt.Printf("程序执行耗费时间 %vs", end-start)
}
协程方法
package main
import (
"fmt"
"time"
)
func putNum(intChan chan int) {
for i := 1; i <= 80000; i++ {
intChan <- i
}
//关闭
close(intChan)
}
func priceNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var num int
var flag bool
var ok bool
for {
//time.Sleep(10 * time.Millisecond)
//判断num是不是素数
num, ok = <-intChan
if !ok {
break
}
flag = true //假设是素数
for i := 2; i < num; i++ {
if num%i == 0 { //说明该num不是素数
flag = false
break
}
}
if flag {
//如果是素数,将这个数放入 primeChan
primeChan <- num
}
}
//fmt.Printf("有一个 primeNum 协程因为取不到数据,退出\\n")
//这里不能关闭 primeChan
//退出管道写入一个标志位
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 20000) //放入结果
exitChan := make(chan bool, 4) //4个
start := time.Now().Unix()
//开启一个协程,向intChan 翻入 1-8000 个数
go putNum(intChan)
//开启4个协程, 从 intChan 取出数据,并且判断是否为素数, 如果是,就4
//放入到 primeChan
for i := 0; i < 4; i++ {
priceNum(intChan, primeChan, exitChan)
}
//主线程进行处理
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
end := time.Now().Unix()
fmt.Printf("使用协程耗时:=> %vs \\n", end-start)
//当我们从 exitChan 取出了 4个结果我们就可以放心的关闭 primeChan
close(primeChan)
}()
//遍历primeChan, 把结果取出
for {
_, ok := <-primeChan
if !ok {
break
}
//fmt.Printf("素数=%d \\n", res)
}
fmt.Printf("主线程退出")
close(exitChan)
}
4. 练习题目
1. 启动一个协程, 将 1-2000 的数放入到一个 channel 中,比如 numChan
2. 启动 8 个协程, 从 numChan 取出数据(比如n), 并且计算 1 +++ .. n 的值, 并且存放到 resChan
3. 最后8个协程完成工作后 ,再遍历 resChan, 显示结果 如 res[1] = 1, ,,,res[10] = 55
4. 注意: 考虑 resChan chan int 是否合适
channel 的注意事项和细节
1. channel 可以声明为只读的 , 或者只写的性质
2. channel 只读和只写的最佳时间案例
3. 使用 select 可以解决从管道独立数据的阻塞问题
select 的案例
package main
import (
"fmt"
"time"
)
func main() {
//使用 select 可以解决管道阻塞问题
//1. 定义一个管道, 10 个数据 int
//2. 定义一个管道,5 个数据 string
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello " + fmt.Sprintf("%d", i)
}
//传统的管道在遍历的时候, 如果不关闭,就会导致阻塞从而导致 deadlock
//问题,在于实际开发过程中,可能我们不确定什么时候关闭管道
for {
select {
//注意: 这里如果 intChan 一直没有关闭,不会一直阻塞,而 deadlock
//, 会自动到下一个 case 匹配
case v := <-intChan:
fmt.Printf("从 intChan 中读取到数据 %d \\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从 stringChan 中读取到数据 %s \\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到, 不玩了\\n")
return
}
}
}
4. goroutine 中只用recover, 解决协程出现 panic , 导致程序崩溃问题
示例代码
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Printf("hello world \\n")
}
}
func test() {
//这里我们可以使用错误处理机制 defer + recover
defer func() {
//捕获test抛出的 panic
if err := recover(); err != nil {
fmt.Printf("test() 发生错误 \\n")
}
}()
//定义一个 map
var myMap map[int]string
//myMap = make(map[int]string, 0)
myMap[0] = "golang" //panic: assignment to entry in nil map
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Printf("main() ok=%v \\n", i)
time.Sleep(time.Second)
}
}
说明: 如果我们启动了一个协程,但是这个协程出现了 panic , 我们没有捕获这个 panic , 进行处理,
这样即使这个协程发生的问题,但是线程任然不受影响, 可以继续执行。
反射能够解决什么问题(应用场景)
1. 定义两个匿名函数
test1 := func(v1 int, v2 int) {
t.Log(v1, v2)
}
test2 := func(v1 int, v2 int, s tring) {
t.Log(v1, v2, s)
}
2. 定义一个适配器函数做统一的处理, 大致结构如下:
bridge := func (call interface{}. args.. interface{}) {
// 内容
}
//实现调用 test1 对应函数
bridge(test1, 1, 2)
//实现调用 test2 对应函数
bridge(test2, 1, 2, "test2")
3. 要求使用反射机智完成(note; 学习 reflect 后, 解决)
反射得重要函数和概念
1. reflect.TypeOf(变量名), 获取变量的类型, 返回 reflect.Type 类型
2. reflect.ValueOf(变量名)。 获取变量的值, 返回 reflect.Value 类型 reflect.Value 是一个结构体类型。 通过 refect.Value , 可以获取到该函数的很多信息。
3. 变量, interface {} 和 reflect.Value 是一个相互转换的,这个点在实际开发中, 回经常使用到,画出示意图
反射的应用场景
反射常见应用场景有两种
1. 不知道接口调用那个函数, 根据传入的参数在运行时候确定调用的具体接口, 这种需要对函数或方法反射,例如这种桥接模式, 比如之前提到问题
func bridge(funcPtr interface{}, args ...interface{})
第一个函数 funcPrt 以接口的行是传入函数指针, 函数参数 args 以可变参数的形式传入, bridge 函数中可以用反射来动态执行 funcPtr 函数
快速入门案例
1. 编写一个案例, 演示对 (基本数据类型, inteface{}, reflect.Value) 进行反射的基本操作。
演示代码
package main
import (
"fmt"
"reflect"
)
//演示反射
func reflectTest(b interface{}) {
//通过反射获取变量的 type, kind, 值
//1.先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType = ", rTyp)
rVal := reflect.ValueOf(b)
n1 := 10
n2 := 2 + n1
fmt.Printf("rVal = %v, type = %T\\n", rVal, rVal)
fmt.Println("n2 = ", n2)
n3 := rVal.Int() + 1
fmt.Println("n3 = ", n3)
}
func main() {
//1. 编写一个案例
//演示对(基本数据类型, interface{} , reflect.Value)进行反射操作
//1.先定义一个 int
var num int = 1000
reflectTest(num)
}
2. 编写一个哪里,演示对(结构体类型, interface{}, reflect.Value) 进行反射的基本操作
演示代码
package main
import (
"fmt"
"reflect"
)
//演示反射
func reflectTest(b interface{}) {
//通过反射获取变量的 type, kind, 值
//1.先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType = ", rTyp)
rVal := reflect.ValueOf(b)
n1 := 10
n2 := 2 + n1
fmt.Printf("rVal = %v, type = %T\\n", rVal, rVal)
fmt.Println("n2 = ", n2)
n3 := rVal.Int() + 1
fmt.Println("n3 = ", n3)
}
//演示反射
func reflectTest2(b interface{}) {
//通过反射获取变量的 type, kind, 值
//1.先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType = ", rTyp)
rVal := reflect.ValueOf(b)
iv := rVal.Interface()
fmt.Printf("iv = %v iV = %T\\n", iv, iv)
}
type Student struct {
Name string
Age int
}
func main() {
//1. 编写一个案例
//演示对(基本数据类型, interface{} , reflect.Value)进行反射操作
//1.先定义一个 int
var num int = 1000
reflectTest(num)
stu := Student{
Name: "Tom",
Age: 20,
}
reflectTest2(stu)
}
以上就是golang学习手册全部内容,感谢大家支持自学php网。