安装链接(非官网):https://studygolang.com/dl
终端输入: go env
, 查看镜像资源
更改一个软件下载源:
go env -w GOPROXY=https://goproxy.cn,direct
export GOPROXY=https://goproxy.cn
设置 GO111MODULE
为打开: go env -w GO111MODULE=on
安装工具: go get -g -v golang.org/x/tools/cmd/goimports
安装 go语言编辑器 : GoLand
点击settings进行配置相关的环境。
GO111MODULE
, 目前的go依赖管理。选择对应的Vgo,然后 Environment填: https://goproxy.cn,direct 更换下载源。创建项目:
设置 proxy: Settings-> Go -> Go Modules (vgo),勾选 Enable Go Modules (vgo) integration 以启用 Go Modules,并在 envirment 输入框中输入 :
https://goproxy.cn,direct
小问题
// fmt.Println(a...: 123) 这个 a... 其实是 Println 参数的名字
// 是goland显示的。 可以在设置里关掉.
// 1. 在设置中搜索 parameter hint(参数提示), 2020.1.1 版本中是在 Inlay Hints 下的 Go,看到 Show parameter hint 把对钩点掉,然后应用就可以了。
使用vscode, 需要安装一堆插件(自动提示安装)
package main
// 函数外也可以定义 变量,但只能使用 var定义
// 另外这是属于 包内变量(作用域是包内部), 没有全局变量的说法
var aa = 33
// 还可以这种写法
var (
bb int = 1
cc string = "123"
dd float32 = 2.01
)
func variable() {
var a, d int = 1, 2
// var a, d = 1, 2 还可以根据后面赋值的类型赋值,不必声明,编译器可推测变量类型
// var a, b, c , s = 3, 4, true, "def"
// p := 10 还可以代替 var 关键字
var b int
var s string = "123"
fmt.Println(a, b, s)
fmt.Printf("%d %q\n", a, s) // 格式打印, 且定义的变量必须使用!
fmt.Println(aa)
}
var a, b = int = 3, 4
var c int = math.Sqrt(a * a + b * b) 错误
var c int = int(math.Sqrt(float64(a*a + b * b))) 正确
func consts() {
const filename string = "123.txt"
// 显示指明类型
const a, b float32 = 3, 4
const b float32 = 4
// 这里必须加上 float64
var c int = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(a, b, filename, c)
}
func consts() {
const filename string = "123.txt"
const a, b = 3, 4
// 这样声明的话, 不需要, a, b是文本, 就自己会当成float来用~
var c int = int(math.Sqrt(a*a + b*b))
fmt.Println(a, b, filename, c)
}
// 枚举类型
func enums() {
const(
cpp = 0 // app = iota 表示自增值,下面的都不需要复值了。 _ 可以跳过一个值,你放说java不要了,它的位置写为 _
java = 1
python = 2
golang = 3
)
// 利用 iota 实现复杂的
const (
b = 1 << (10 * iota)
kb
mb
gb
pb
)
// 结果按照公式进行
1 1024 1048576 1073741824 1099511627776
}
if else 语句
if 条件{
...
} else if {
...
} else {
...
}
// 支持条件内赋值, 变量的作用域就在 if else语句中
if a, b := 2, 3; a == 1{
fmt.Println(a)
} else {
fmt.Println(b)
fmt.Println("cannot print", nil)
}
switch 语句
// switch语句不需要每句后面加上break, 每个case后面都有break, 除非使用 fallthrough
var result int
var op = 1
switch op {
case 1:
result = 1
// 中断程序执行,报错 panic
panic("error")
case 2:
result = 2
case 3:
result = 3
default:
result = 4
}
for 语句
sum := 0
for a := 1; i <= 100; i++ {
sum += i
}
// 复杂的读取文件,并逐行打印
func forT(filename string) {
file, err := os.Open(filename)
// 判断是否异常
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(file)
// 这里只有这个 结束条件。 直接省略掉 ; (起始条件,递增条件都没有), 充当while 使用
for scanner.Scan() {
fmt.Println(scanner.Text())
}
// 还可以省略 递增条件. 相当于死循环
for {
fmt.Println("abc")
}
// 1. 返回单值
func eval(a, b int, op string) int {}
// 2. 返回两个int值
func div(a, b int, op string) (int, int) {
return a / b, a % b
}
// 3. 还可以给返回值取名字:
func div(a, b int, op string) (q, r int) {
return a / b, a % b
}
// 当执行函数时, 可自动生成
q, r := div(13, 3)
// 4. 当定义出来后,直接自动返回
func div(a, b int, op string) (q, r int) {
q := a / b
r := a % b
return // 自动返回 q, r: 不建议这样, 当代码比较长,可读性太差
}
// 5. 按照上面的例子,在其它函数中调用
// 这是错误的,因为这是返回的两个值不能像python那样 return div(a, b)[0] 表示返回第一个值
return div(a, b)
// 在这样也是错误的! 哈哈。。 go语言中定义两个变量,必须都要使用起来,编译错误
q, r := div(a, b)
return q
// 正确写法, _ 表示不需要这个值
q, _ := div(a, b)
return q
// 6. 一般来讲,多返回值不是乱用的, 第二个返回值,一般是返回的错误,例如file, err := os.Open(filename)
func div(a, b int, op string) (int, error) {
if a == 1 {
return 1, nil
} else {
return 0, fmt.Errorf("err....") // 出错时,返回异常error
}
}
aaa, err := div(2, 3, "123")
fmt.Println(aaa, err)
// 结果会被打印出来
// 0 err....123
// 7. 函数作为形参
// op 是个函数, 有两个int 类型参数, 返回值是 int 类型
func apply(op func(int, int) int, a, b int) {
return op(a, b)
// 也可以获取原来的函数名
p := reflect.ValueOf(op).Pointer() // 获取函数真正的指针
opName := runtime.FuncForPc(p).Name() // 获取函数名
}
// 8. 匿名函数
// apply 是个函数, apply(func (int, int) int, a, b int), 下面调用apply,中间函数参数传递是匿名函数,定义在内部
apply(
func(a int, b int) int {
return a
}, 3, 4)
)
// 函数参数: go 语言中没有 默认参数,可选参数, 函数重载等 比较复杂的函数参数类型
// 有可变参数列表
func sum(numbers ...int) int{ // numbers 可以接受多个int 值
s := 0
for i := range numbers {
s += numsbers[i]
}
return s
}
// 执行
sum(1, 2, 3, 4, 5)
// C语言中
int a = 100;
// 第1行代码中 * 用来指明 p 是一个指针变量,第2行代码中 * 用来获取指针指向的数据。
int *p_a = &a;
*p_a = 200;
// go 中
var a int = 2
var pa *int =&a
*pa = 3
// go语言中的指针简单再: 不能进行运算。 C语言中,获取列表的头指针,可以一直不断的进行运算加下去。
函数中的 参数传递:go语言中只有值传递一种传递方式!
go语言中,值传递, 还可以进行指针传递。相当于引用的作用。
// 指针的作用 又 强于 引用。 一般来讲 java,Python,内建的基本类型是 值传递,比方说 :
// Python中 a = 5, 传递到函数内,不会影响该 变量的值。这是值传递。
// list 列表等,容器对象 和 自定义类型对象,都是 引用传递
// 而 go语言中的, 指针传递。
var a int
func f(pa *int){} // 传递 &a 进去,就算是 int类型, 也可以函数内修改该变量
举个简单的例子: 交换两个数
var a, b int = 3, 4
// 值传递 是交换不了的, 只是将a, b 的值拷贝一份, 形参 也叫 a, b
func swap(a, b int) {
a, b = b, a
}
swap(a, b)
// 指针传递: 可以正确交换
func swap(a, b *int) {
*a, *b = *b, *a
}
swap(&a, &b)
// 当 传递的是 object 时, 也是值传递。
var cache Cache
func f(cache Cache){}
// cache 是一个 Cache 类型的对象。 它保存的并不是真正内存中的数据,而是指向内存中数据的指针,
// 所以,值传递完全没有问题, 相当于将 对象的 指针拷贝了一份, 操作的是同一个对象
那么看到一个对象, 到底该用值类型,还是指针类型传递呢? 在 对象封装 中会讲。
func array() {
// 一位数组定义方式
var arr1 [5]int
arr2 := [3]int {1, 2, 3}
arr3 := [...]int{2, 4, 6, 8 ,10}
// 二维数组: 4行5列
var grid [4][5]int
fmt.Println(arr1)
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(grid)
// 1.遍历
for i:= 0; i < len(arr3); i++{
fmt.Print(arr3[i], " ")
}
// 2.遍历: range
fmt.Print("\n")
for i := range arr3 {
fmt.Println(arr3[i], " ")
}
// 相当于 python中的 enumerate(), 遍历 索引和 值
// 如果只想要值, 可以 for _, v := range arr3, 下划线代替
for i, v := range arr3 {
fmt.Println(i, v)
}
// 为什么要使用range, 1. 简洁美观。 2.
}
数组是值类型!
// arr [5]int 这样传递的是 值类型。 也就是传入的数组 必须是 5长度才行。
func printArray(arr [5]int) {
arr[0] = 100
for i, v := range arr {
fmt.Println(i, v)
}
}
printArray(arr1) // 对
printArray(arr2) // 错误
// 因为是值传递, 相当于将数组整体 五个元素做一个拷贝,再函数内对数组进行更改,不会影响到外部!
// 要想其他语言中可以改变原数组,就需要传递指针了
func printArray(arr *[5]int) {} // 数组内通过 arr[0] = 100 可以修改外部数组的值
printArray(&arr1)
go语言中一般不直接使用数组: 而是切片~
arr := []int {1, 2, 3, 4 ,5 ,6 ,7 ,8} // 注意和数组区别,不用加 ...
// 1. 这里切片和 python是一样的 s = [2, 3, 4, 5], 左闭右开
s := arr[2:6]
// 2. 其他写法: arr[:6], arr[2:], arr[:] 和python一样
// 3. slice 不是值类型, 它的内部有个数据结构, slice 是对arr 的一个视图,看到数组中指定的内容
// 将 数组的切片(slice) 传递到 函数中, 对其修改是可以改变外部数组的
func updateSlice(s []int) {
s[0] = 100
}
updateSlice(s)
fmt.Println(arr) // 会变为100
// 4. 如果要对原数组进行操作,就可以使用 slice切片 即可
updateSlice(arr[:])
// 5. 可以对 切片进行切片。 得到的是以当前切片为准的 切片
arr := [...]int {1, 2, 3, 4 ,5 ,6 ,7 ,8}
s1 := arr[2:6] // s = [3, 4, 5, 6]
s2 := s1[3:5] // s = [6 ,7]
s3 := s1[3:] // s = [6]
// 可以看到 上面你那个超额去取, 因为底层都是对 arr的视图, 当s2规定[3:5] 超过s1时,会取到隐藏的部分
// s3 取 s1[3:], 默认只能看到显示的 s1的部分,也就是只取到一个 6
// 二维切片
var a [][]string
// 或者
b = make([][]string, 0)
// 赋值
a = [][]string{[]string{"abc", "efg"}, []string{"abc", "mln"}}
fmt.Println(a)
// 结果
[[abc efg] [abc mln]]
slice
param: ptr 指向的是切片的开头元素
param: len 当前的切片长度, 当s[6] 大于该长度就会报错。
parma: cap(capacity), 指针对 ptr 到 最后有多长。
// 需要注意:
// 1. slice 可以向后扩展,但是没法向前扩展, 当切片的第一个 元素大于 len 时,就会报错。s2 = s1[6:7] 就会报错,不会进行扩展
// 2. 可以打印出, 获取 len 和 cap 的值
fmt.Printf("s1= %v, len(s1)=%d, cap(s1)= %d\n", s1, len(s1), cap(s1))
// 可以得到结果
s1= [3 4 5 6], len(s1)=4, cap(s1)= 6
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X79rhUOh-1635157982890)(.\slice.png)]
arr := [...]int {1, 2, 3, 4 ,5 ,6 ,7 ,8}
s1 := arr[2:6]
fmt.Printf("s1= %v, len(s1)=%d, cap(s1)= %d\n", s1, len(s1), cap(s1))
s2 := append(s1, 10)
s3 := append(s2, 11)
s4 := append(s2, 12)
s5 := append(s4, 13)
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(s4)
fmt.Println(s5)
fmt.Println(arr)
// 得到结果为:
/*
s1= [3 4 5 6], len(s1)=4, cap(s1)= 6
[3 4 5 6]
[3 4 5 6 10]
[3 4 5 6 10 11]
[3 4 5 6 10 11 12]
[3 4 5 6 10 11 12 13]
[1 2 3 4 5 6 10 11]
可以看到 s1 的最大结果为 cap = 6, 所以后面对切片的操作时, arr中,10, 11 覆盖了 7, 8。
而后面插入 12, 13 ,并不会影响到 arr 了。
总结: 当添加元素时,如果超越了 cap, 系统会重新分配更大的底层数组,并将原来的元素拷过去(参考上面的 底层结构)
另外: 由于值传递, 必须接收 append 的返回值。 因为 append 时,可能底层数组改变里,len,cap等都可能改变, 需要重新接收新的返回
*/
// 1. 声明 变量的类型为 slice
var s []int // 默认为 nil, go中变量 定义不初始化也行
// 2. 第二种方式: 建立了一个 array, 有声明了 s1 是 array的 slice
s1 := []int {2, 4, 6, 8}
// 3. 建立定长的 slice
s2 := make([]int, 16, 32) // len = 16, cap = 32, 不值当cap,默认为 len长
// 4. 可以进行append
for i := 0; i < 100; i++ {
fmt.Printf("len(s)=%d, cap(s)= %d\n", len(s), cap(s))
s = append(s, i * 2 + 1)
}
// 每次对底层数组的扩充, 都是乘 2的。打印输出可以看到
len(s)=0, cap(s)= 0
len(s)=1, cap(s)= 1
len(s)=2, cap(s)= 2
len(s)=3, cap(s)= 4
len(s)=4, cap(s)= 4
len(s)=5, cap(s)= 8
len(s)=6, cap(s)= 8
len(s)=7, cap(s)= 8
len(s)=8, cap(s)= 8
len(s)=9, cap(s)= 16
len(s)=10, cap(s)= 16
len(s)=11, cap(s)= 16
len(s)=12, cap(s)= 16
len(s)=13, cap(s)= 16
len(s)=14, cap(s)= 16
len(s)=15, cap(s)= 16
len(s)=16, cap(s)= 16
len(s)=17, cap(s)= 32
len(s)=18, cap(s)= 32
len(s)=19, cap(s)= 32
//5. copy: 将s1 复制到 s2
copy(s2, s1)
// 6. delete
// 删除索引值 3的元素: s2[:3] + s2[4:]
// append第二个值是一个变长参数列表,也就是可以 一次添加多个值。当加入的是一个 slice, 后面加上 ... 即可
// 另外 删除一个元素,不影响 cap, 大小不变, len - 1
s2 = append(s2[:3], s2[4:]...)
// 7. 删除 头的元素
front := s2[0]
s2 := [1:]
// 8. 删除尾元素
tail := s2[len(s2 - 1)]
s2 := s2[:len(s2) - 1]
###1. 基础内容
// 1. 建立map
// 方法1.
m := map[string] string {
"name": "xiaoming",
"course": "golang",
"age": "18",
}
// 方法2. go 语言中 nil (相当于 Python的 None), 是可以参与运算的,相当于空 map 使用
m1 := make(map[string]int) // m1 == empty map 空map
var m2 map[string] int // m2 == nil 空
// 2. 遍历map, 同样可以使用 下划线 _ 代替不想要的
// 注意: 每次遍历的得到的顺序不一定相同, 因为 map的 结构
for k, v := range m {
fmt.Println(k, v)
}
// 3. 获取值
courseName := m["course"]
// 当获取不存在的值的时候,得到的就是空串, 对应数据类型的空值。 go语言变量不初始化,也能用
courseName, exist := m["course"] // 第二个变量 exist 可以判断当前值是否存在,返回 bool值
// 标准写法
if courseName, exist := m["course"]; exist {
fmt.Println(courseName)
} else {
fmt.Println("error")
}
// 4. 删除元素
delete(m, "name")
import "fmt"
func lengthOfLongestSubstring(s string) int {
charMap := map[byte] int {}
i := 0
rst := 0
byteS := []byte(s) // 转换为字节 slice
for j := 0; j < len(byteS); j++ {
if _, ok := charMap[byteS[j]]; !ok {
charMap[byteS[j]] = 1
} else {
// 当前元素以重复出现,先计算当前窗口大小
if rst < j - i {
rst = j - i
}
// 去除重复
for _, ok := charMap[byteS[j]]; ok ; _, ok = charMap[byteS[j]]{
delete(charMap, byteS[i])
i += 1
}
// 添加当前元素到窗口
charMap[byteS[j]] = 1
}
}
// 最后还会有一个位置没有判断
if rst < len(byteS) - i{
return len(byteS) - i
} else {
return rst
}
}
// 哈哈, 属实脑洞大开, 去除重复那里,本该是个 while循环, 用for 来写, 就要搞懂 ; ; 三部分都是啥,让我给写对了
// 如果 s[j] 在 map 里,就一直循环, i++
for charMap[s[j]]; ok ; _, ok = charMap[s[j]]{}
// 还可以直接这样: 默认找不到元素是 返回 0
for charMap[s[j]] != 0 {}
// 注意点:
// 1. 最开始是定义了 i,j变量,才能在整个代码中使用!, for则只能在相应的 {} 代码块中使用
// 2. charMap := map[byte] int {} 需要注意这个定义, go语言对字符的处理
// 另外需要注意的是
###(三)字符和字符处理
func strings() {
s := "ABC我是初学者!" // UTF-8 编码, rune 类型
fmt.Println(s, len(s)) // 打印长度为 19!
// 1. 转为字节类型输出
for _, b := range []byte(s) {
fmt.Printf("%X ", b )
}
// 一字节的表示 打印如下: 中文为 3字节表示
// 比如 E6 88 91 表示 "我" , 是 UTF-8编码
// 41 42 43 E6 88 91 E6 98 AF E5 88 9D E5 AD A6 E8 80 85 21
fmt.Println()
// 2. 直接输出
for i, ch := range s {
fmt.Printf("(%d, %X)", i, ch)
}
// 结果: (0, 41)(1, 42)(2, 43)(3, 6211)(6, 662F)(9, 521D)(12, 5B66)(15, 8005)(18, 21)
// ch 为rune 类型(等价于 int32)四字节整数。
// 6211表示 "我", 是unicode编码。 且可以看到 下标递增三。
// "我": 过程,将s(string) 字符 utf-8解码:E6 88 91, 转为 unicode 6211, 再放到 rune类型 四字节类型中保存
fmt.Println()
// 3. 调用utf8这个库
fmt.Println(utf8.RuneCountInString(s))
// 打印结果为 9
// 拿到字节数组
bytes := []byte(s)
// 返回每个字符 和字符大小,英文1,中文3。 解码
for len(bytes) > 0 {
ch, size := utf8.DecodeRune(bytes)
bytes = bytes[size:]
fmt.Printf("%c ", ch)
}
// 结果 A B C 我 是 初 学 者 !
fmt.Println()
// 转为rune 类型
for i, ch := range []rune(s) {
fmt.Printf("(%d, %c)", i, ch)
}
// 结果 (0, A)(1, B)(2, C)(3, 我)(4, 是)(5, 初)(6, 学)(7, 者)(8, !)
}
utf8.RuneCountInString()
获得字符数量package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "ABC我刚开始学go!"
var runeS []rune = []rune(s) // 打印 11,每个rune 是int32,4字节
fmt.Println(len(runeS))
var byteS []byte = []byte(s) // 打印 21,中文字符集占三个字节
fmt.Println(len(byteS))
// 返回每个字符 和字符大小,英文1,中文3。 解码
for len(byteS) > 0 {
ch, size := utf8.DecodeRune(byteS)
byteS = byteS[size:]
fmt.Printf("%c ", ch)
}
// 结果 A B C 我 是 初 学 者 !
// 3. 调用utf8这个库, 可以得到想要的长度 11
fmt.Println(utf8.RuneCountInString(s))
}
// 注意: a := []byte(s) 或 a := []rune(s) 或者 s(字符串默认都是 rune类型)
// 他们 使用 a[2], s[2] 得到的都是数字, 字符串在go里的特殊表示
// 将字节(byte) slice, 表示为 字符slice (rune)
func lengthOfLongestSubstring(s string) int {
charMap := map[rune] int {}
i := 0
rst := 0
runeS := []rune(s)
for j, ch := range runeS { // 这里的ch 也是 数字, 或者直接 for j := 0; j < len(runsS); j++
if _, ok := charMap[ch]; !ok {
charMap[ch] = 1
} else {
// 当前元素以重复出现,先计算当前窗口大小
if rst < j - i {
rst = j - i
}
// 去除重复
for charMap[ch] != 0{
delete(charMap, runeS[i])
i += 1
}
// 添加当前元素到窗口
charMap[ch] = 1
}
}
// 最后还会有一个位置没有判断
if rst < len(runeS) - i{
return len(runeS) - i
} else {
return rst
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKl44vPr-1635157982892)(./rune_byte_range.png)]
// strings 库下有很多操作
老师没细讲,后序再来记录
###1.基本内容
// 定义一个树的节点类型
type TreeNode struct {
Left, Right *TreeNode // 指针类型
Value int
}
func main() {
// 1. 定义
var root1 TreeNode
root2 := TreeNode{}
// 初始化
root1 = TreeNode{val: 3}
root1.left = &root2 // 注意 指针,传递地址
// 注意go语言中,不管是实例 还是 属性,都是 . 像 c++ 可能是 ->
root1.right = &TreeNode{val: 4}
fmt.Println(root1.val)
// 2. 还可以定义一个 slice
nodes := []TreeNode {
// 省略掉 初始化数据
{val: 3},
{},
{nil, &root1, 6},
}
fmt.Println(nodes)
//结果: [{ 3} { 0} { 0xc000004078 6}]
}
###2. 构造函数?
结构体是没有 构造函数的。
工厂函数
,实际上就是普通函数,比如// 工厂函数
func createNode(val int) *TreeNode{
return &TreeNode{val: val}
}
// 学过c++ 的话可以看到这里有个典型的错误, TreeNode{val: val} 是在函数内构建的局部变量
// 返回局部变量的指针, 程序就挂了!
// go 语言中不会挂~ 局部变量 也可以返回给别人用
root1.left.right = createNode(9) // 这样也是可以的
// TreeNode 结构体
type TreeNode struct {
left, right *TreeNode
val int
}
// 给结构体定义方法
func (node TreeNode) print() { // (node TreeNode) 接收者
fmt.Println(node.val)
}
// 调用
root1.print()
有个接收者, 这里就是 node TreeNode。
go 语言的这个定义,其实和普通的函数方法差不多,只是一种语法, 实际上就是普通的函数,等价于:
func print(node TreeNode) { // 这里是传值,go 只有传值
fmt.Println(node.val)
}
// 调用
print(root1)
需要注意,上面的写法都是传值的, 函数内部修改不会堆 外部对象造成影响,等于拷贝。
func (node *TreeNode) print() { // 传指针
fmt.Println(node.val)
}
// 调用: 这点是比较人性化的。 虽然上面变成了传指针,但是调用起来是不变的。实际调用会自动传递地址进去
root1.print()
// 相同的,如果函数定义的就是传值, 你传递进去一个地址~ 也不会影响,会自动从地址把值取出拷贝。
总结: go很聪明,知道你函数调用时 是要值,还是要指针。
type TreeNode struct {
left, right *TreeNode
val int
}
// 给结构体定义方法
func (node *TreeNode) print() { // (node TreeNode) 接收者
fmt.Println(node.val)
}
func (node *TreeNode) setValue(val int) {
if node == nil {
fmt.Println(“Setting val to nil node!”)
}
node.val = val
}
var pRoot *TreeNode
// nil 指针,调用方法
pRoot.setValue(200) // 到这里可以打印出来,说明能够调用方法,但是不能执行下面的 node.val = val 操作~
pRoot = &root1
pRoot.setValue(300)
pRoot.print()
* 对于值接收者,指针接收者的问题
* 要改变内容,必须使用指针接收者
* 结构体过大也考虑使用指针接收者
* 一致性: 如果有指针接收者, 最好都是指针接收者。
* 值接收者是 go 特有的。 指针接收者,c++, java引用,python(self),也类似指针。
### 5. 实现一个遍历二叉树的方法
* 前序遍历: [力扣原题](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/submissions/)
```GO
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
// 递归解题
func preorderTraversal(root *TreeNode) (vals []int) { // 这里相当于定义了 vals 这个变量
var preorder func(*TreeNode)
preorder = func(node *TreeNode) {
if node == nil {
return
}
vals = append(vals, node.Val)
preorder(node.Left)
preorder(node.Right)
}
preorder(root)
return
}
// 另外需要注意, 初始化 slice
zxc := []int{}
var cvb []int
fmt.Println(zxc == nil, cvb == nil) // 结果 false, true
// 写成TreeNode的 方法
func (node *TreeNode) traverse() {
if node == nil {
return
}
node.print()
node.left.traverse()
node.right.traverse()
}
###6. 包和封装
通过对函数的命名,来区分函数时可见或者 不可见等。 名字一般使用 CamelCase: 单词首字母大写连在一起。
针对包来说:
public
private
包:
tree目录
node.go //定义方法 和 节点结构体 package tree
traverse.go // 为了清晰,还可以分开, 把遍历节点的方法单独放一个文件里。都可以找到
entry // 目录
entry.go // 将main函数移动到这里, package main : 一个目录下只能有一个包。所有要新建一个目录,取名 package main, main函数写进来。
// entry.go 中import 语句, 是导入 tree 目录的。
// entry.go 中import 语句, 是导入 tree 目录的。
// 创建变量时需要, tree.TreeNode
var root tree.TreeNode
关于导包的一点东西(自己感觉得):
// 目录结构
01
testing
retriever.go // 包名是 package testing111, 定义了一个 type Retriever struct{}
downloader.go // 导入包,并调用包里的内容
import "01/testing" // 导入到路径名
testing111.Retriever{} // 调用使用包名
另外: 再一个包中定义的结构体,结构体方法, 只能定义在同一包下,否则会报错:invalid receiver *testing111.Retriever (type not defined in this package)
在 上面的testing 目录下再创建一个文件 , 同样 package testing111 里面可以成功定义方法,因为属于同一个包。
在其他语言中有 继承的概念,可以扩展已有的类型
go语言中是没有继承得
go可以扩充 系统类型
还可以扩充 别人的类型
有两种方式:
下面的代码。依照上面定义的包结构(tree目录)
// (根据go语言习惯,包名叫 tree, 将TreeNode 改写为 Node, tree.TreeNode,叫起来太重复了)
// 且首字母大写 表示Public的方法。所有方法名改为 首字母大写。
type Node struct {
Val int
Left *TreeNode
Right *TreeNode
}
// 目标: 让节点对象 支持中序遍历(Node 实现的是前序遍历)
// 定义自己的节点
type MyTreeNode struct {
node *tree.Node // 保存节点类型的指针。
}
func (myNode *MyTreeNode) InOrder() {
if myNode == nil || myNode.node == nil { // 自己为空,或者 包装的node为 空,都不进行下去。
return
}
// 需要将 Node 包装成 myTreeNode,调用中序遍历方法,进行中序遍历
left := myTreeNode{myNode.node.Left}
right := myTreeNode{myNode.node.right} // myTreeNode{myNode.node.Left}.InOrder() 这里不能这样写,因为传递的是指针,必须要用一个变量接收
left.InOrder()
myNode.node.Print()
right.InOrder()
}
// 报错信息展示: myTreeNode{myNode.node.Left}.InOrder()
/*
cannot call pointer method on myTreeNode literal
cannot take the address of myTreeNode literal
无法在 myTreeNode 文字上调用指针方法
不能取 myTreeNode 文字的地址
*/
实现一个 队列。
// 定义一个 Queue。 目录结构如下
queue // 目录
entry // 目录
main.go // 程序入口
queue.go // 定义结构体和 方法
queue.go
package queue
// 定义一个 队列
type Queue []int
func (q *Queue) Push(v int) {
// go语言的特性,指针是可以被改变的。 q传进来的是一个指针,*q 去操作它指向的对象
// 然后 q 指向的对象,再变为 指向 append 新产生的对象
// *q 相当于 python 中的 self,表示对象本身但是 go 里面, self(q)指向的却被变为了新产生的对象~!
*q = append(*q, v)
}
// 先进先出队列
func (q *Queue) Pop() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
package main
import (
"fmt"
"01/queue"
)
func main() {
q := queue.Queue{1}
q.Push(2)
q.Push(3)
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
}
1
2
false
3
true
// 符合预期
内嵌
// 前面组合的方式:
type MyTreeNode struct {
node *tree.Node // 保存节点类型的指针。
}
// 内嵌就是 去掉 node, 相当于是go的一个语法糖,省下一些代码的量,简洁一些
type MyTreeNode struct {
*tree.Node // 保存节点类型的指针。
}
// 内嵌的好处是,节省代码。 看下之前组合中 中序遍历里的代码
left := myTreeNode{myNode.node.Left}
right := myTreeNode{myNode.node.Left}
// 使用内嵌后, myNode 就相当于是 有了原来Node 的所有属性,和方法,还有自身定义的属性和方法。
// 直接写, myNode.Left, 省去了 中间的node
left := myTreeNode{myNode.Left}
right := myTreeNode{myNode.right}
使用内嵌
其他语言中的重写,体现在go中, 也可以在 myTreeNode 中定义 TreeNode同名的 方法,提示是: shadowed。
直接调用 mynode.方法名,将会是调用重写后的方法。 但是隐藏的 mynode.TreeNode.方法名,将可以调用原来的方法。
java中有个体现多态的概念: 父类父类引用指向子类对象
(大概是上转型):
父类对象不可以访问,子类方法。
而上转型对象,既可以访问父类方法(通过 super. 来调用),也可以访问子类继承或隐藏的成员变量,或者调用子类继承的方法(父类方法)或者子类重写的实例方法。进而,一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。
就是go的安装路径
默认在 ~/go(unix, linux)
, %USERPROFILE%\go (windows)
都是在用户目录下
实践操作GOPATH(切记先关掉 GO111MOODULE):
整个系统更改: go env -w GO111MOODULE = "off"
, 当前窗口:export GO111MOODULE = "off"
// 创建一个目录叫做 gopathtest,来作为 GOPATH
// 目录结构
gopathtest
src // 这个目录必须有,且必须这个名字。src下面可以放各种代码
// 1. go env -w GOPATH = "目录" 更改系统整个GOPATH
// 2. export GPPATH="目录" 临时更改。仅限这个终端窗口
// 拉去一个项目
go get -u go.uber.org/zap // 库名 zap
// 再次看到, src 目录下就会出现
go.uber.org
写代码测试:zaptest.go
import "go.uber.org/zap"
func main() {
log, _ := zap.NewProduction()
log.Warn("warning test")
}
问题: 上面导图的 zap库,若有两个项目都需要使用这个库,但是所需版本不同,那么就做不到了
// 目录结构
gopathtest
src
go.uber.org
zap // zap 库
project1 // 项目1
vendor
go.uber.org // 放上一份
project2 // 项目2
vendor
go.uber.org // 放上一份
当开启 GO111MOODULE, 再次 : go get -u go.uber.org/zap
下载这个包
go get -u go.uber.org/[email protected]
gomodetest // 创建项目名
go.mod
go.sum // 介绍可参看: https://my.oschina.net/renhc/blog/3171035
zaptest.go // 测试代码
module gomodetest
go 1.17
require go.uber.org/zap v1.19.1
require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
)
C:\Users\Administrator\go\pkg\mod\go.uber.org\[email protected]
go mod init +模块名字 生成 mod 文件
go mode tidy, 删除未使用的模块 或者 添加已使用但go.mod中不存在的模块
go mod init
go get
, 或者直接写代码, build 时候自动就会把依赖拉取加进去go mod init, go build ./...
// 像下面这样的目录结构:
01
arrays.go
maps.go
slices.go
funcs.go
// 01是我建的一个项目, 下面每一个是我学习各个部分基础知识,创建的文件。
当执行 go build + 任何一个文件都可以编译通过,但是当执行 go build, 一次编译所有文件就会出错:
main redeclaration in this block : 很明显,每个文件下面都有一个 mian函数,是不对的。
// 将每个文件单独放到一个文件夹下,才能有单独的 main函数。
01
arrays
arrays.go
maps
maps.go
slices
slices.go
funcs
func.go
go build ./...
就可以编译通过。把当前目录(空)以及所有子目录中的文件 build,编译。go install ./...
来生成。// 先瞅一下,看完后面再回来看这个结构。 Python也有这个 “鸭子类型”, 感觉go这里就是 把“静态类型”变成 “动态类型”了。
// 这个接口, 不用去考虑 Traversal 指的是啥, 也不用管这个接口 有没有被实现(java中。 go中就不能这样想)只要满足了 Traverse()方法。 就可以是它。
// “给人的感觉是: 好像不需要在定义变量时给他确定类型,而是运行时动态确定一样的感觉~, ” getTraversal()传递啥类型过来, 满足接口定义的方法, 接口就可以是该类型。
type Traversal interface {
Traverse()
}
func main() {
traversal := getTraversal()
traversal.Traverse()
}
写一个文件:downloader.go
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
// 定义一个请求链接的函数
func retrieve(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
bytes, _ := ioutil.ReadAll(resp.Body)
// 转字符串输出
return string(bytes)
}
func main() {
fmt.Println(retrieve("https://www.imooc.com"))
}
// 当前目录结构
downloader.go
infra // infra包下,有个方法是 解析url的
urlretriever.go
urlretriever.go:
package infra
import (
"io/ioutil"
"net/http"
)
// 为了引入接口,先将它定义为一个 结构体
type Retriever struct {}
func (Retriever) Get(url string) string{ // 不需要给接收者起名字,因为用不到
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
bytes, _ := ioutil.ReadAll(resp.Body)
// 转字符串输出
return string(bytes)
}
downloader.go :
package main
import (
"fmt"
"01/infra"
)
func main() {
retriever := infra.Retriever{} // 构建一个对象,调用即可
fmt.Println(retriever.Get("https://www.imooc.com"))
}
新的问题: retriever := infra.Retriever{}
这句话,并不一定一直都是调用 infra.Retriever{}, 它不属于一个逻辑,更像是一个配置,把它提取出来。
dowloader.go 中这样写
func getRetriever() infra.Retriever {
return infra.Retriever{}
}
func main() {
retriever := getRetriever() // 通过方法去获取结构体对象
fmt.Println(retriever.Get("https://www.imooc.com"))
}
新的问题: 这样写,还存在一个很强的耦合, retriever的类型不能换。 他一直保持是 infra.Retriever 类型。retriever := getRetriever()
始终相当于是 var retriever infra.Retriever = getRetriever()
是infra.Retriever类型的。 如果我们另外还有一个团队(testing),他们也有一个 解析url的功能。那么,要想调用这个 testing.Retriever,就很麻烦了,需要从头到尾的去改 getRetriever方法为 testing.Retriever类型
// 新的目录结构, 加入 testing部门
downloader.go
infra // infra包下,有个方法是 解析url的
urlretriever.go
testing
retriever.go
retriever.go:
package main
type Retriever struct{}
func (Retriever) Get(url string) string {
return "fask content" // 和infra部门实现内容不一样
}
那么最终修改: (引出接口概念)
package main
import (
// "01/infra"
"fmt"
"01/testing"
)
// 返回值为接口类型,必须带有 Get方法
func getRetriever() retriever { // 返回类型 retriever
return testing.Retriever{}
}
// 定义接口
type retriever interface {
// Get
Get(string) string
}
func main() {
var r retriever = getRetriever()
fmt.Println(r.Get("https://www.imooc.com"))
}
// 此时完全解耦, 我们只需要 更改 getRetriever() 方法里的 返回值为 infra.Retriever{},就可以调用 infar里的go了,其他都不用动。
testing.Retriever{}
或者 infra.Retriever{}
testing.Retriever does not implement retriever (missing Get1 method)
之前就说过,go 没有像 面向对象的语言那样。它只支持封装, 像继承,和多态所要完成的事儿,在go中都是 使用接口代替来完成。所以go语言中的接口,要比其他语言中灵活会很多。
鸭子类型:简单讲: “像鸭子走路,像鸭子叫(长得像鸭子),那么他就被认为是鸭子”。不同人对事物的认知不一样。它是不是一个“鸭子”,要从我的观点去看,满足我的需要,我就认为它就是“鸭子”。 编程里: 只要你能提供,我接口里所定义的那些能力(方法),我就认为你是“鸭子”吗满足我的需要, 我可以赋值为 你的类型, 跟你的这些能力互动。
另外go语言属于是 结构化类型系统, 类似 duck typing ,因为 duck typing当时提的时候,一定要动态绑定!(参考Python解释型语言 鸭子类型),但go 语言是编译时就绑定了~ 只能说类似。 看到这里,可以再去理解开篇的那段话和代码了。
Python中的 duck typing
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SG8CZ2og-1635157982895)(.\py_duck.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nND91X9P-1635157982896)(./c++_duck.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-if6uPutl-1635157982898)(./java_duck.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ol3j3bmA-1635157982898)(./go_duck.png)]
###3. 接口的定义和实现
// 目录结构
retriever
mock
mockretriever.go //定义者
main.go // 使用者
package main
import (
"01/retriever/mock"
"fmt"
)
type Retriever interface {
Get(url string) string
}
func download(r Retriever) string {
return r.Get("www.imooc.com")
}
func main() {
var r Retriever
r = mock.Retriever{"this is me"}
fmt.Println(download(r))
}
package mock
type Retriever struct{
Contents string
}
// 实现get方法。(因为有了使用者,定义者默认算实现了 Get接口)
func (r Retriever) Get(url string) string {
return r.Contents
}
###4.接口的值类型
var r Retriever
fmt.Printf("%T, %v\n", r, r)
r = mock.Retriever{"this is me"}
fmt.Printf("%T, %v\n", r, r)
// 打印结果
<nil>, <nil>
mock.Retriever, {this is me} // r 内部有个类型,还有个值
// 将 函数Get 改为接收指针
func (r *Retriever) Get(url string) string {
return r.Contents
}
var r Retriever
fmt.Printf("%T, %v\n", r, r)
r = &mock.Retriever{"this is me"} // 取地址
fmt.Printf("%T, %v\n", r, r)
// 打印结果
<nil>, <nil>
*mock.Retriever, &{this is me} // 都是指针
值传递 是 mock.Retriever{"this is me"} 生成了一个对象,然后拷贝到 r肚子里。
指针传递 是将对象的指针放到 r 肚子里。
查看 r 的类型
// switch 可以获取 r的 类型
switch v := r.(type) {
case *mock.Retriever:
fmt.Println(v.Contents)
}
// Type assertion (类型断言)
mockRetriever, ok := r.(*mock.Retriever) // 当为值时,就是r.(mock.Retriever)
// ok用来判断 类型判断是否正确
fmt.Println(ok, mockRetriever.Contents)
接口变量(比如r)
任何类型: interface{}
q := []interface{} {1, "ahah", '1'}
// q就变成了,类似Python中的列表。 可以接收任何类型。
// 组合现有的两个接口
type RetrieverPoster interface {
Retriever // 实现Get
Poster // 实现Post
}
s := &mock.Retriever{"this is me"} // mock.Retriever 中再定义一个 Post方法
func session(s RetrieverPoster){
s.Get()
s.Post()
}
// 直接定义接口
type Rf interface {
Post(url string) string
Get()
}
// 部分代码
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
Writer
Closer
}
stringer: 相当于java的 tostring
fmt/print.go 下面有个 stringer。
type Stringer interface {
String() string
}
// 我们自己的对象,只要实现了方法(String()),就可以实现接口了。
fmt.Println(对象) // 就会调用了。
Reader
io/io.go 下面有个 Reader, Writer
type Reader interface { // 实现读取文件的功能
Read(p []byte) (n int, err error)
}
type Writer interface { // 实现写文件的功能: byte slice, string 都能作为 Reader, Writer
Write(p []byte) (n int, err error)
}
抽象,任何实现和 Read, Write 方法的对象,都等于实现了这两个接口,可以进行读写操作。
Writer
###1. 基本概念
package main
import "fmt"
// 实现累加
func adder() func(int) int {
sum := 0
var a func(i int) int = func (i int) int {
sum += i
return sum
}
return a
}
func main() {
a := adder()
for i := 0; i < 10; i++ {
fmt.Println(a(i))
}
}
// 里面的函数只能写成这样
1. var a func(i int) int = func (i int) int , 定义一个变量接收
2. 更简洁的使用 匿名函数, 省去掉名字
func adder() func(int) int {
sum := 0
return func (i int) int {
sum += i
return sum
}
}
上面的例子就是演示了一个闭包的概念(Python中都学习过相关的概念)
Python中的闭包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLAivqUt-1635157982899)(./py_Closure.png)]
c++ 中的闭包(比较新的语法,c++ 14以后)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gqe5ou6e-1635157982900)(./c++_Closure.png.png)]
java中的定义要严格一些: 使用final 关键字,定义一个class。holder,里面就一个 value
java中的函数不是一等公民,不支持函数作为参数和返回值,所以使用一个对象进行模拟一个函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tvLjX2Oz-1635157982901)(./java_Closure.png)]
package main
import "fmt"
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
func fibonacci() intGen {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
type intGen func() int // 函数类型, 是一个类型就能实现接口~~ go语言灵活的地方
// 其实就是一种语法糖,前面讲过。
// (g intGen) , 就是 g.Read() 调用。 不写这种,就是 Read(g) 调用。没有很特殊,就是普通函数
// 所以 即使是函数对象,也可以实现接口(实现方法, 方法被使用者使用,等于实现接口)
func (g intGen) Read(p []byte) (n int, err error) {
// n: 写了多少字节, error异常
// next 调用 g,读取下一个 斐波那契数
next := g()
// 控制 大于 10000停止
if next > 10000 {
return 0, io.EOF
}
// 调用Sprintf,将对应 字节数,计算出来,转字符串并加入换行,存 p里
s := fmt.Sprintf("%d\n", next)
// 通过 NewReader 来代理写入 p的操作
return strings.NewReader(s).Read(p) // strings.NewReader(s) 返回一个 Reader对象, 他调用Read() 方法,将s写入p
}
func printFileContents(reader io.Reader) {
scanner := bufio.NewScanner(reader) // 点进源码可以发现,这里将 io.Reader又进行包装为 Scanner对象
for scanner.Scan() { // 循环判断, Scan() Text() 都是 Scanner 对象提供的方法,进行输出
fmt.Println(scanner.Text())
}
}
func main() {
f := fibonacci()
// printFileContents 函数,将 参数 reader 定义为 io.Reader 接口类型。 所以需要 f实现了 Reader方法
// 然后 printFileContents 函数 内部不断的操作 调用 Read方法,写入数据,然后读取。
// 且 Read 内部实现了一个 每次将 一个 斐波那契数加入到 p 里面的功能。
printFileContents(f)
// 总体过程, 1. 将 f 对象作为参数传递给 printFileContents() 函数
// 2. 内部使用 io.Reader 接口类型变量接收 f, f 实现了 Reader方法
// 3. 内部不断的调用 next := go(), 来写入下一个 斐波那契数 供打印。
// 难点。 1. 对 io.Reader 接口实现的理解, 2. 为一个函数类型 实现 Reade 方法(间接就是实现了io.Reader 接口)
}
package main
import "fmt"
// 定义节点
type TreeNode struct {
left, right *TreeNode
val int
}
// 中序遍历
func (node *TreeNode) TraverseFunc(f func(*TreeNode)){ // 传递一个函数
if node == nil {
return
}
node.left.TraverseFunc(f)
f(node)
node.right.TraverseFunc(f)
}
func main() {
root1 := TreeNode{val: 3}
// 对 nodeCnt 进行累加计数
nodeCnt := 0
// 传递匿名函数实现累加计数, 中序遍历统计节点个数
root1.TraverseFunc(func (node * TreeNode){
nodeCnt ++
})
fmt.Println(nodeCnt)
}
确保调用在函数结束时发生
defer列表 并且满足先进后出(就是defer 语句出现的顺序,先出现的,后执行)
参数在defer语句时计算
func tryDefer() {
defer fmt.Println(1) // 先输出2, 后输出1
fmt.Println(2)
}
func tryDefer() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
}
// 保证先进后出。 所以打印结果是 3 2 1
func tryDefer() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
return // panic("error") 这都不影响,输出结果依然为 3 2 1 即使后面程序进行中断, 依然不影响预期执行
fmt.Println(4)
}
实战进行 一个 文件的写操作(创建一个文件,写入fibonacci数列)
资源管理
: 创建文件 --> 关闭文件 . 写入缓存 --> Flush() 成对出现, 我们就可以在打开文件后面 直接 defer close()package main
import (
"01/functional/fib"
"bufio"
"fmt"
"os"
)
func writeFile(filename string) {
file, err := os.Create(filename)
if err != nil {
panic(err)
}
defer file.Close() // 完成时关闭文件
// 直接写文件比较慢, 利用bufio
writer := bufio.NewWriter(file) // buffer 缓冲,先写入到内存中去,到一定大小时,一次性写入硬盘中
defer writer.Flush() // 只写入buffer 缓冲,还需要导进去
f := fib.Fibonacci()
for i := 0; i < 20; i++ {
fmt.Fprintln(writer, f())
}
}
func main() {
writeFile("fib.txt")
}
参数在defer语句时计算:
func forDefer() {
for i := 0; i < 100; i++ {
defer fmt.Print(i, " ")
if i == 30 {
panic("error")
}
}
}
打印结果为:
// 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 panic: error
// 先输出30,再输出 29..28..0 满足先进后出。并且,不是打印 30个 30.说明 i 参数的值,是在程序执行 defer 语句时,已经计算好的。
一个超级有意思的例子:
package main
import "fmt"
func f1() int {
x := 5
defer func() {
x += 1
}()
return x
}
func f2() (x int){
defer func() {
x++
}()
return 5
}
// 输出结果为 5, 6
func f1() *int {
x := 5
defer func() {
x += 1
}()
return &x
}
a := f1()
fmt.Println(*a)
// 结果是 6. 只能说是 值传递。 return 返回的是 数值5. x后面右 ++
go语言的错误处理,其实主要就是围绕着 error的。 比方说 file, err := os.Create(filename)。
func writeFile(filename string) {
file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666) // 0666 linux 文件权限, 加了这个os.O_EXCL,文件存在会打开不了。 人为制造一个错误异常
if err != nil {
// panic(err) 这里不能单纯的进行 panic, 中断程序。可以进行细致的错误处理
// 1. 进行打印输出, 再返回。不让异常中断程序
// fmt.Println("file already exists ", err)
// return
// 2. 打开OpenFile()函数源码会发现, If there is an error, it will be of type *PathError, err 是接口类型,实现Error()方法,这里报错将返回的是 *PathError
if pathError, ok := err.(*os.PathError); !ok { // 类型断言,看是否会报这个类型的错
panic(err) // 当送进来的不是 *os.PathError 类型,直接 panic
} else {
fmt.Println(pathError.Op, // 输出一些 关于pathError的信息
pathError.Path,
pathError.Err)
}
return
}
// 打印结果: open fib.txt The file exists. 对应上面的三个 打印。
err = errors.New("this is a custom error")
, 也可以为期定义 Error方法, 实现接口。如何实现统一的错误处理逻辑
errhandling
defer
fib.txt // fib文件
filelisting
web.go // webserver, 访问 fib 文件
最初版本(实现逻辑)
// web.go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
// 实现一个显示文件的 webserver
func main() {
http.HandleFunc("/list/",
func (writer http.ResponseWriter,
request *http.Request) {
// 拿到list 后面具体的文件名,或者目录
fileName := request.URL.Path[len("/list/"):] // /list/fib.txt, 通过切片,只要后面的。fib.txt
// 打开文件
fibPath := fmt.Sprintf("%s%s", "../defer/", fileName) // 拿到文件名,拼接路径
file, err := os.Open(fibPath)
if err != nil {
panic(err)
}
defer file.Close()
// 读取文件
all, err := ioutil.ReadAll(file)
if err != nil {
panic(err)
}
// 内容写入到写入到 ResponseWriter 里去
writer.Write(all)
})
err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}
尝试处理错误:
file, err := os.Open(fibPath)
if err != nil {
http.Error(writer,
err.Error(),
http.StatusInternalServerError) // code
return
}
// 最后执行错误的 url, 结果将在 浏览器中显示,而不会在后台打印一堆错误信息
// 浏览器显示内容 open ../defer/fib.txta: The system cannot find the file specified.
将这些出错信息,包装成一个外部的 err。
filelisting
listing
handeler.go // 将函数处理的逻辑提取出来
web.go // mian 函
package listing
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func HandFileList(writer http.ResponseWriter,
request *http.Request) error {
// 拿到list 后面具体的文件名,或者目录
fileName := request.URL.Path[len("/list/"):] // /list/fib.txt, 通过切片,只要后面的。fib.txt
// 打开文件
fibPath := fmt.Sprintf("%s%s", "../defer/", fileName)
file, err := os.Open(fibPath)
if err != nil {
return err
}
defer file.Close()
// 读取文件
all, err := ioutil.ReadAll(file)
if err != nil {
return err
}
// 内容写入到写入到 ResponseWriter 里去
writer.Write(all)
return nil
}
package main
import (
"01/errhandling/filelisting/listing"
"net/http"
"os"
)
// 定义一个函数类型, 返回 error
type appHandler func(writer http.ResponseWriter,
request *http.Request) error
// 再定义一个函数,功能是返回 HandleFunc()参数需要的函数
func errWrapper(
handler appHandler) func(
http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter,
request *http.Request) {
err := handler(writer, request)
if err != nil {
code := http.StatusOK
switch {
// 如果是文件不存在的错误
case os.IsNotExist(err):
http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
code = http.StatusNotFound
// 没权限
case os.IsPermission(err):
code = http.StatusForbidden
// 默认
default:
code = http.StatusInternalServerError
}
http.Error(writer,
http.StatusText(code), code)
}
}
}
// 实现一个显示文件的 webserver
func main() {
http.HandleFunc("/list/", errWrapper(listing.HandFileList)) // 处理逻辑函数,抽象出来
err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}
panic
recover
package main
import (
"fmt"
)
func tryRecover() {
defer func() {
r := recover() // func recover() interface{} 。可以看到recover() 是个任何类型
if err, ok := r.(error); ok {
fmt.Println("Error occurred: ", err)
} else {
panic(r)
}
}() // 定义匿名函数,并且调用
b := 0
a := 5 / b
fmt.Println(a)
}
func main() {
tryRecover()
}
// 结果
Error occurred: runtime error: integer divide by zero // 程序并不会异常终止
如果直接panic(123), 不是异常类型,那么 ,还将panic出去 123
func tryRecover() {
defer func() {
r := recover() // func recover() interface{} 。可以看到recover() 是个任何类型
if err, ok := r.(error); ok {
fmt.Println("Error occurred: ", err)
} else {
panic(r)
}
}() // 定义匿名函数,并且调用
// b := 0
// a := 5 / b
// fmt.Println(a)
panic(123)
}
// r的值 就是 123
学习了上面的 panic 和 recover 之后, 我们再来看一下
http.HandleFunc("/", errWrapper(listing.HandFileList)) // 把原先的 /list/ 改为了 /
fileName := request.URL.Path[len("/list/"):]
2021/10/07 17:17:34 http: panic serving [::1]:65487: runtime error: slice bounds out of range [6:4]
goroutine 6 [running]:
net/http.(*conn).serve.func1(0xc00005ab40)
D:/software/go/go1.15/src/net/http/server.go:1801 +0x147
panic(0x66a120, 0xc0000aa080)
D:/software/go/go1.15/src/runtime/panic.go:975 +0x3e9
01/errhandling/filelisting/listing.HandFileList(0x6d9200, 0xc0000ae0e0, 0xc0000a8000, 0x0, 0x0)
D:/software/go/go1.15/src/net/http/server.go:1801 +0x147
这里:func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed)
}
}()
那么我们的代码 func errWrapper,也要加上一个 defer, recover。异常终止程序时进行操作。
func errWrapper(
handler appHandler) func(
http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter,
request *http.Request) {
// !!!加上defer 处理异常
defer func() {
if r != nil {
r := recover()
log.Printf("Panic: %v", r)
http.Error(writer,
http.StatusText(http.StatusInternalServerError), // 返回服务器出错状态码
http.StatusInternalServerError)
}
}()
err := handler(writer, request)
if err != nil {
code := http.StatusOK
switch {
// 如果是文件不存在的错误
case os.IsNotExist(err):
http.Error(writer, http.StatusText(http.StatusNotFound), http.StatusNotFound)
code = http.StatusNotFound
// 没权限
case os.IsPermission(err):
code = http.StatusForbidden
// 默认
default:
code = http.StatusInternalServerError
}
http.Error(writer,
http.StatusText(code), code)
}
}
}
后序还有主要内容,看不太懂,超出基础了,都不了解这些方法啥的。先放弃了。
goroutine
goroutine 模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkzj62Vm-1635157982902)(./goroutine.png)]
初见
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
for {
fmt.Printf("Hello from goroutine %d\n", i) // 打印属于io操作,这句话会进行阻塞,交出控制权
}
}(i)
}
// 睡眠10秒。 主程序 和 开的协程是一起执行的。当主程序执行完毕 协程还未执行,main()函数结束,整个程序就结束了
time.Sleep(time.Millisecond*100)
}
协程 Coroutine:
将上面的程序改为一个数组(不存在io等操作,就不会进行切换,让出控制权)
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func() {
for {
a[i]++
runtime.Gosched()
}
}()
}
// 睡眠10秒。 主程序 和 开的协程是一起执行的。当主程序执行完毕 协程还未执行,main()函数结束,整个程序就结束了
time.Sleep(time.Millisecond)
fmt.Println(a)
}
两个数据冲突的地方:使用 go run -race goroutine.go, 查看数据冲突
任何函数只需要加上 go ,就能送给调度器执行
goroutine 可能的切换点(非抢占式)
连续开启1000个协程, go语言会开启大概七个线程,而调度器会根据你的机器的核数,只有相应的一部分线程处于运行活跃状态。(top命令 linux系统中查看)
channel : goroutine 与 goroutine 之间的双向通道。
var c chan int // c == nil
a := make(chan int) // 可以写为 a := make(chan int, 3) 加缓存,指定空间
var channels [10]chan int, 定义 数组,每个元素是一个channel
func createWorker(id int) chan int {
// 函数内创建 channel
c := make(chan int)
// 必须去 创建一个 协程,去处理 channel, 如果不把 for 循环定义到 func 里, 程序就死循环了! 一直等待接收数据。
go func() {
for {
fmt.Println(<- c)
}
}()
// 返回 channel
return c
}
// createWorker 函数的作用就是创建返回一个 channel。 他没有做任何事情,真正的处理是在内部的 func 里。
// 还可以规定方向, 外部的人只能收: 箭头标志
func worker(id int) chan <- int {}
c := make(chan int, 3) 规定大小,创建缓冲区,对性能是有一定得帮助的。可以累积导入三个 数值,才去切换,被其他协程接收。
还可以 停止发送: close(a) a为channel。 这是发送发使用 close(). 接收方 还可以接收数据, 不过接收的都是 0 (a chan int)。
for n := range a { // range 会自动判断 是否发送方调用close()
fmt.Println(n)
}
channel 的理论基础: Communication Sequential Process(CSP)
go语言作者的话: Don’t communicate by sharing memory; share memory by communicating.
翻译: 不要通过共享内存来进行通信; 通过通信来共享内存。
前面当中的例子, 主程序中, 最后使用 time.Sleep() 来进行等待, 是 mian函数外的 写成任务进行执行。这种方式不是很好, 我们需要一个专业的方式,去等待 channel 任务结束, 然后结束 main() 函数。
// 定义channel
var in chan int
var done chan bool
// 1.发送者
in <- 1
in <- 2
<- done
<- done
// 接收者
var n int
n <- in
n <- in
done <- true
done <- true
这样的写法,就不需要加上 Sleep() 了, 因为 发送者发送 in 完毕后, 还需要接收 接收者 done 发送的(没有实际接收), 这就导致了,需要等待 接收者发送数据, 从而实现了 等待任务的 接收者 in的任务结束了!
* 有了理论基础,下面就可以去 编写小例子, 串起来实践下了。
```go
package main
import (
"fmt"
)
func doWork(id int,
in chan int, done chan bool) {
// range 读取 in channel 数据
for n := range in {
// 处理 in 接收的数据
fmt.Printf("Worker %d received %c\n",
id, n)
// 反馈信息, 已经处理完了
done <- true
}
}
type worker struct {
in chan int
done chan bool
}
// 创建工作
func createWorker(id int) worker {
w := worker {
in: make(chan int),
done: make(chan bool),
}
go doWork(id, w.in, w.done)
return w
}
// 例子
func chanDemo() {
// 创建十个 工人
var workers [10]worker
for i := 0; i < 10; i++ {
workers[i] = createWorker(i)
}
// 给每个工人分配任务, 传入一个值
for i, worker := range workers {
worker.in <- 'A' + i
}
// 给每个工人分配任务, 再传入一个值
for i, worker := range workers {
worker.in <- 'a' + i
}
// wait for all oh them, 统一等待所有的 done 的返回, 使上面的发送方 可以并发的去执行
// 因为上面 每个 doWork接收两次值, 这里需要 同样就收两次
for _, worker := range workers {
<- worker.done
<- worker.done
}
}
func main() {
chanDemo()
}
// 这样写并不会正确执行!!, 原因在于 dowork 中处理了 一个数据之后, 会 <- done, 而 chanDemo 里没有去即使处理这个 , 那么 dowork 会一直 卡在 <- done , 等着别人去接收, 而 chanDome 没有接收,反而又去 发送一组数据到 in <- . 导致双方等待,死锁。
解决方式可以是
1. doWork 中: go func() {done <- true} // 再次创建一个协程,执行这句。
2. 或者:设置缓存!
createWorker() 函数中:
w := worker {
in: make(chan int),
done: make(chan bool),
}
可以设置缓存, 当 done <- true 时,被立即等待 别人去收
w := worker {
in: make(chan int),
done: make(chan bool, 2), // 可以缓存
}
3. 或者是 实际业务不会有太复杂的并发。 这里可以把接收 分开, 不要一次等待接收,两批任务的结果
chanDome() 中:
// 给每个工人分配任务, 传入一个值
for i, worker := range workers {
worker.in <- 'A' + i
}
// 发送一组后, 立马等待结果
for _, worker := range workers {
<- worker.done
}
// 给每个工人分配任务, 再传入一个值
for i, worker := range workers {
worker.in <- 'a' + i
}
// 发送一组后, 立马等待结果
for _, worker := range workers {
<- worker.done
}
使用 sync.WaitGroup 进行等待
var wg sync.WaitGroup
// 提供三个方法
wg.Add() // 接收int 类型: 任务数 用在接收者
wg.Done() // 任务处理结束标志 用在处理者
wg.Wait() // 等待所有任务结束 用在处理者
将上述代码的 done channel 去掉改用 WaitGroup
package main
import (
"fmt"
"sync"
)
func doWork(id int,
in chan int, wg *sync.WaitGroup) {
// range 读取 in channel 数据
for n := range in {
// 处理 in 接收的数据
fmt.Printf("Worker %d received %c\n",
id, n)
// 反馈信息, 已经处理完了
wg.Done()
}
}
type worker struct {
in chan int
// 添加 WaitGroup
wg *sync.WaitGroup // 使用指针, 调用同一份。 不能是单独的
}
// 创建工作
func createWorker(id int, wg *sync.WaitGroup) worker {
w := worker {
in: make(chan int),
wg: wg,
}
go doWork(id, w.in, w.wg)
return w
}
// 例子
func chanDemo() {
// 创建 WaitGroup
var wg sync.WaitGroup
// 创建十个 工人
var workers [10]worker
for i := 0; i < 10; i++ {
workers[i] = createWorker(i, &wg)
}
// 我们下面设置的 一共20个任务
wg.Add(20)
// 给每个工人分配任务, 传入一个值
for i, worker := range workers {
worker.in <- ‘A’ + i
// 除了一次性Add(20), 当不知道总的 任务数时, 可以再这里,每创建一个 Add(1)
}
// 给每个工人分配任务, 再传入一个值
for i, worker := range workers {
worker.in <- ‘a’ + i
}
// 等待
wg.Wait()
}
func main() {
chanDemo()
}
上代码
package main
import "fmt"
type TreeNode struct {
Left *TreeNode
Right *TreeNode
Val int
}
//1. 使用函数式编程,遍历树. 中序遍历
func (node *TreeNode) TraverseFunc(f func(*TreeNode)) {
if node == nil {
return
}
node.Left.TraverseFunc(f)
f(node)
node.Right.TraverseFunc(f)
}
// 2. 使用 channel进行 数的遍历
// 和之前的例子一样, TraverseWithChannel() 相当于是 createWorker 的作用。创建一个 channel 并返回
func (node *TreeNode) TraverseWithChannel() chan *TreeNode {
out := make(chan *TreeNode)
// 开启goroutine,写入node节点
go func() {
node.TraverseFunc(func(node *TreeNode) {
out <- node
})
// 关闭
close(out)
}()
return out
}
func main() {
// 0. 先进行创建 树
root := TreeNode{nil, nil, 1}
root.Left = &TreeNode{nil, nil, 5}
root.Right = &TreeNode{Val: 4}
root.Right.Left = new(TreeNode)
root.Left.Right = &TreeNode{nil, nil, 3}
// 1. 调用 函数式遍历,f 匿名函数,统计节点个数
count := 0
root.TraverseFunc(func(node *TreeNode) {
count++
fmt.Println(node.Val)
})
fmt.Println("Tree has Node count: ", count)
// 2. 通过channel
c:= root.TraverseWithChannel()
// 通过range 就可以遍历节点了, 统计元素值最大的
maxNodeVal := 0
for node := range c {
if node.Val > maxNodeVal {
maxNodeVal = node.Val
}
}
fmt.Println("Max Node Value: ", maxNodeVal)
}
// 这里有些像 Python中的 yield。 out <- node , 之后切换, range 里拿到后, 执行再切换。
// go语言的channel 可以做更加复杂的事情, 不过这里就有些相当于 yield 的功能了
老师讲的例子比较复杂, 还讲到了计时器, 是个很有用的场景例子。 我只是听了听后面的。
package main
import (
"fmt"
"math/rand"
"time"
)
// 创建channel
func generator() chan int {
out := make(chan int)
i := 0
go func() {
for {
// 随机sleep 1500毫秒
time.Sleep(
time.Duration(rand.Intn(1500)) *
time.Millisecond)
out <- i
i++
}
}()
return out
}
func main() {
var c1, c2 = generator(), generator()
// 假如我们想从 c1,c2 收数据, 并且谁来的快 收谁,就需要使用 select 了
//n1 := <- c1
//n2 := <- c2
// 就好像是做了一个非阻塞式的处理~ 立即返回结果
for {
select {
case n := <- c1:
fmt.Println("Receive from c1: ", n)
case n := <- c2:
fmt.Println("Receive from c2: ", n)
//default: // 不加 default 就会陷入阻塞,等待数据中
// fmt.Println(“No value received”)
}
}
}
总结:
上面提到的复杂的例子中, 没有使用任何锁,都是通过channel进行通信共享数据的。这就是 CSP机制。
也有传统的机制, Mutex, Cond, WaitGroup也算的是
Mutex (互斥量)
package main
import (
"fmt"
"sync"
"time"
)
// 制作原子操作的 int类型。注意: 系统是有 atomic 的原子操作的。atomic.AddInt32() 等,保证"线程安全",
// go中就是 在多个并发执行的 goroutine中 保证安全
type atomicInt struct {
val int
lock sync.Mutex
}
// 构建两个函数
func (a *atomicInt) increment() {
// 用锁来保护起来
a.lock.Lock()
defer a.lock.Unlock()
a.val++
}
func (a *atomicInt) get() int {
// 加锁解锁
a.lock.Lock()
defer a.lock.Unlock()
return a.val
}
func main() {
var a atomicInt
a.increment()
go func() {
a.increment()
}()
time.Sleep(time.Millisecond)
fmt.Println(a.get())
}
g 语言中很少使用传统的 同步机制, 尽量使用 channel 进行通信。 传统的同步机制都是 通过共享内存进行通信的, 需要加锁去保护。
有一个文件:
// maze.in 其中第一行是 文件的行列数。 第二行空格隔开。
// 0 表示路, 1表示墙。 随机给 起点(start) 和 终点(end)。 标记路径
6 5
0 1 0 0 0
0 0 0 1 0
0 1 0 1 0
1 1 1 0 0
0 1 0 0 1
0 1 0 0 0
代码实现
package main
import (
"fmt"
"os"
)
// 1. 从文件读取并返回 二维数组
func readMaze(filename string) [][]int {
// 打开文件
file, err := os.Open(filename)
if err != nil {
panic(err)
}
// 读取二维数组的 行列
var row, col int
fmt.Fscanf(file, "%d %d", &row, &col) // 要加上取地址, 值传递改不了外界的
maze := make([][]int, row) // 创建二维数组,实际上是一维数组, 元素又是一维数组
for i := range maze {
maze[i] = make([]int, col)
for j := range maze[i] { // 读取索引
fmt.Fscanf(file, "%d", &maze[i][j])
}
}
return maze
}
// 定义坐标点的表示
type point struct {
i, j int
}
// 定义 上左下右的 方向
var dirs = [4]point {
{-1, 0}, {0, -1}, {1, 0}, {0, 1}}
// 实现 坐标相加的方法
func (p point) add(r point) point{ // 使用值类型, 返回新的
return point{p.i + r.i, p.j + r.j}
}
// 判断 point 位置是否越界
func (p point) at(grid [][]int) (int, bool) {
if p.i < 0 || p.i >= len(grid) {
return 0, false
}
if p.j < 0 || p.j >= len(grid[p.i]) {
return 0, false
}
return grid[p.i][p.j], true
}
func walk(maze [][]int, start, end point) [][]int{
// 建立坐标路线
steps := make([][]int, len(maze))
for i := range steps {
steps[i] = make([]int, len(maze[i]))
}
// 广度优先求解
// 1. 建立队列
Q := []point {start}
for len(Q) > 0 {
// 拿到队头元素
cur := Q[0]
Q = Q[1:]
// 获取元素的位置
curSteps, _ := cur.at(steps)
// 走到终点了:
if cur == end { break }
// 上下左右走
for _, dir := range dirs {
// 获取新节点位置
next := cur.add(dir)
// 判断该位置是否合法, 越界或者撞墙
val, ok := next.at(maze)
if !ok || val == 1 { continue }
// 判断这个点是否走过了
val, ok = next.at(steps)
if !ok || val != 0 { continue }
// 等于原点也不行
if next == start {continue}
// 下面开始做事, 将下一元素 步数记录,然后添加进Q队列
steps[next.i][next.j] =
curSteps + 1
Q = append(Q, next)
}
}
return steps
}
func main() {
// 获取二维 slice
maze := readMaze("maze/maze.in")
// 打印
//for _, row := range maze {
// for _, num := range row {
// fmt.Printf("%d ", num)
// }
// fmt.Println()
//}
// 走迷宫
steps := walk(maze, point{0, 0}, point{
2,2})
// 打印结果
for _, row := range steps {
for _, num := range row {
fmt.Printf("%4d", num) // %4d 最后输出占三位
}
fmt.Println()
}
}
go语言作为一门面向网络,面向服务的, 高并发编程语言, 对 http 的封装是非常良好的。
可以作为服务器
也可以作为客户端: 爬虫,请求
package main
import (
"fmt"
"net/http"
"net/http/httputil"
)
func main() {
request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
request.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")
// 自定义 Client
client := http.Client{
CheckRedirect: func(
req *http.Request,
via []*http.Request) error {
fmt.Println("Redirect: ", req)
return nil
},
}
resp, err := client.Do(request)
//resp, err := http.DefaultClient.Do(request)
//resp, err := http.Get("http://www.imooc.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
s, err := httputil.DumpResponse(resp, true) // 一次性解析出来
if err != nil {
panic(err)
}
fmt.Printf("%s\n", s)
}
http 服务器性能分析
bufio
log
encoding/json
regexp
time
strings/ math/ rand
如何查看标准库的文档
自己起一个服务器
https://studygolang.com/pkgdoc
创建一个队列: https://studygolang.com/pkgdoc
字符串转数字,数字转字符串: https://blog.csdn.net/weixin_34185560/article/details/91438265
字符串拼接(删除其中一个字符)
st := "12345678"
st = st[:2] + st[3:8]
fmt.Println(st)
// 1245678
// 拼接
rst := []string {“1”, “2”, “3”}
fmt.Println(strings.Join(rst, “,”))
// 1,2,3
package main
import "fmt"
type a struct {
b int
}
type w struct {
s *a
}
func index(x int) { // 1. 定义参数可以不使用
fmt.Println(123)
}
func main() {
index(2) // 定义参数可以不使用
aa := a{1}
w := w{&aa}
w.s.b = 3 // 2. 结构体也是值传递, 需要使用指针传递,才是同一份
fmt.Println(w.s) // 结果 &{3}
fmt.Println(aa) // 结果 {3}
}
[][]int{
// 建立坐标路线
steps := make([][]int, len(maze))
for i := range steps {
steps[i] = make([]int, len(maze[i]))
}
// 广度优先求解
// 1. 建立队列
Q := []point {start}
for len(Q) > 0 {
// 拿到队头元素
cur := Q[0]
Q = Q[1:]
// 获取元素的位置
curSteps, _ := cur.at(steps)
// 走到终点了:
if cur == end { break }
// 上下左右走
for _, dir := range dirs {
// 获取新节点位置
next := cur.add(dir)
// 判断该位置是否合法, 越界或者撞墙
val, ok := next.at(maze)
if !ok || val == 1 { continue }
// 判断这个点是否走过了
val, ok = next.at(steps)
if !ok || val != 0 { continue }
// 等于原点也不行
if next == start {continue}
// 下面开始做事, 将下一元素 步数记录,然后添加进Q队列
steps[next.i][next.j] =
curSteps + 1
Q = append(Q, next)
}
}
return steps
}
func main() {
// 获取二维 slice
maze := readMaze(“maze/maze.in”)
// 打印
//for _, row := range maze {
// for _, num := range row {
// fmt.Printf("%d ", num)
// }
// fmt.Println()
//}
// 走迷宫
steps := walk(maze, point{0, 0}, point{
2,2})
// 打印结果
for _, row := range steps {
for _, num := range row {
fmt.Printf("%4d", num) // %4d 最后输出占三位
}
fmt.Println()
}
}
## 第十三章: 标准库
### http库
* go语言作为一门面向网络,面向服务的, 高并发编程语言, 对 http 的封装是非常良好的。
* 可以作为服务器
* 也可以作为客户端: 爬虫,请求
* 使用http客户端发送请求
* 使用http.Client 控制请求头部等
* 使用httputil简化工作
```go
package main
import (
"fmt"
"net/http"
"net/http/httputil"
)
func main() {
request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
request.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")
// 自定义 Client
client := http.Client{
CheckRedirect: func(
req *http.Request,
via []*http.Request) error {
fmt.Println("Redirect: ", req)
return nil
},
}
resp, err := client.Do(request)
//resp, err := http.DefaultClient.Do(request)
//resp, err := http.Get("http://www.imooc.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
s, err := httputil.DumpResponse(resp, true) // 一次性解析出来
if err != nil {
panic(err)
}
fmt.Printf("%s\n", s)
}
http 服务器性能分析
bufio
log
encoding/json
regexp
time
strings/ math/ rand
如何查看标准库的文档
自己起一个服务器
https://studygolang.com/pkgdoc
创建一个队列: https://studygolang.com/pkgdoc
字符串转数字,数字转字符串: https://blog.csdn.net/weixin_34185560/article/details/91438265
字符串拼接(删除其中一个字符)
st := "12345678"
st = st[:2] + st[3:8]
fmt.Println(st)
// 1245678
// 拼接
rst := []string {“1”, “2”, “3”}
fmt.Println(strings.Join(rst, “,”))
// 1,2,3
package main
import "fmt"
type a struct {
b int
}
type w struct {
s *a
}
func index(x int) { // 1. 定义参数可以不使用
fmt.Println(123)
}
func main() {
index(2) // 定义参数可以不使用
aa := a{1}
w := w{&aa}
w.s.b = 3 // 2. 结构体也是值传递, 需要使用指针传递,才是同一份
fmt.Println(w.s) // 结果 &{3}
fmt.Println(aa) // 结果 {3}
}