数组和切片
数组
# 1 定义,初始化,使用
# 2 数组是值类型
数字,字符串,布尔,数组,都是值类型,真正直接存数据
切片,map,指针 引用类型,是个地址,指向了具体的值
# 3 数组长度
# 4 循环打印数组
# 5 多纬数组
# 6 数组定义并赋初值 ,把第99 赋值为1 ,其他都是0
# 数组的长度也是类型的一部分
package main
import "fmt"
// 1 数组是值类型,go语言中函数传参是 copy 传递,复制一份参数,传入 当参数传递,在函数中修改,不会影响原来的
//2 数组长度
func main() {
//1 数组是值类型
//var a [3]int = [3]int{3, 4, 4}
//test(a)
//fmt.Println("外面的", a) // 会不会影响?
// 2 数组长度
//var a =[3]int{1,2,3}
//a :=[3]int{1,2,3}
//a := [...]int{3, 4, 5, 4, 5, 6, 67}
//fmt.Println(a)
//fmt.Printf("%T\n", a)
//fmt.Println(len(a))
// 3 循环打印数组
//a := [...]int{3, 4, 5, 4, 5, 6, 67}
// 基于索引的
//for i := 0; i < len(a); i++ {
// fmt.Println(a[i])
//}
// range 循环,基于迭代的
//for i,_ := range a { // i是索引,v是值
// fmt.Println(i)
// //fmt.Println(v)
//}
// 4 多纬数组
//var a [3][2]int
//var a [3][2]int = [3][2]int{{2, 2}, {3, 4}}
//fmt.Println(a)
//fmt.Println(a[0])
//fmt.Println(a[0][1])
// 循环多维数组
//for i := 0; i < len(a); i++ {
// for j := 0; j < len(a[i]); j++ {
// fmt.Println(a[i][j])
// }
//}
//for _, v := range a {
// for _, v1 := range v {
// fmt.Println(v1)
// }
//}
// 5 数组定义并赋初值 ,把第99 赋值为1 ,其他都是0
//var a [100]int8 = [100]int8{98: 1, 66: 88}
//fmt.Println(a)
}
func test(a [3]int) {
a[0] = 999
fmt.Println(a)
}
切片
# slice:切片是由数组建立的一种方便、灵活且功能强大的包装。切片本身不拥有任何数据。它们只是对现有数组的【引用】
本身不存储数据,是对底层数组的引用
package main
import "fmt"
// 切片
func main() {
//1 切片的定义 中括号中不放任何东西,是切片类型,放个数字,就是数组类型
//var a []int //只定义,没有初始化,是引用类型 是nil类型,等同于python中的None
//if a == nil {
// fmt.Println("我是空")
//}
//fmt.Println(a) // []
//fmt.Println(a[0])
// 1.1 定义并初始化
//var b [10]int //值类型,有默认值
//var a []int = b[:] // 表示a这个切片,对数组进行引用,引用了从0到最后
//fmt.Println(a)
//a[8] = 88
//fmt.Println(a[8])
//fmt.Println(a)
//
//fmt.Println(b)
// 2 切片的取值赋值
//fmt.Println(a[8])
// 3 切片的修改,会影响底层数组; 数组的修改也会影响切片
//b[0] = 11
//fmt.Println(b)
//fmt.Println(a)
// 4 基于数组,获得切片
//var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} //值类型,有默认值
////var a []int = b[0:3] // 表示a这个切片,对数组进行引用,引用了从0到最后
////fmt.Println(a)
////a[0] = 99
////fmt.Println(a)
////fmt.Println(b)
//var a []int = b[6:9]
//fmt.Println(a)
//a[0] = 99
//fmt.Println(a)
//fmt.Println(b)
//b[0] = 88
//fmt.Println(b)
// 5 切片追加值
//var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
//var a []int = b[6:9]
//a[0] = 99
//fmt.Println(b) //[1 2 3 4 5 6 99 8 9 0]
//fmt.Println(a) //[99 8 9]
//
//a = append(a, 66)
//fmt.Println(a) //[99 8 9 66]
//fmt.Println(b) //[1 2 3 4 5 6 99 8 9 66]
// 6 切片的长度和容量
//var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
//var a []int = b[6:9]
//fmt.Println(len(a)) // 3
//fmt.Println(cap(a)) //容量,能存多少 4 ,不是底层数组的大小,取决于切片切数组的位置
// 7 切片如果追加值超过了底层数组长度,会自动扩容
//var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
//var a []int = b[6:9]
//a[0] = 99
//fmt.Println(b) //[1 2 3 4 5 6 99 8 9 0]
//fmt.Println(a) //[99 8 9]
//a = append(a, 66)
//fmt.Println(a) //[99 8 9 66]
//fmt.Println(b) //[1 2 3 4 5 6 99 8 9 66]
//fmt.Println(len(a))
//fmt.Println(cap(a))
// 超过底层数组,追加值
//a = append(a, 77)
//fmt.Println(a) //[99 8 9 66 77]
//fmt.Println(len(a)) // 5
//fmt.Println(cap(a)) // 8 ,超过容量,翻倍扩容(容量在1024内)
//a = append(a, 11, 222, 33, 55)
//fmt.Println(len(a)) // 9
//fmt.Println(cap(a)) // 16 ,超过容量,翻倍扩容(容量在1024内)
// 改底层数组
//b[9] = 999
//fmt.Println(b)
//fmt.Println(a)
// 8 切片定义并初始化 使用数组初始化
// 使用make初始化
//var a []int // 是 nil,没有初始化
//var a []int = make([]int, 3, 4) // 要初始化 ,长度为3,容量为4
//fmt.Println(a) // [0 0 0]
//fmt.Println(len(a))
//fmt.Println(cap(a))
//var a []int = make([]int, 0, 4) // 要初始化 ,长度为0,容量为4
//fmt.Println(a) // [] 有可能是nil,有可能已经初始化完成了,只有初始化完成了才能用
//fmt.Println(len(a))
//fmt.Println(cap(a))
//a = append(a, 5)
//fmt.Println(a)
//fmt.Println(len(a)) //1
//fmt.Println(cap(a)) // 4
// 9 切片的参数传递 引用类型,函数中修改值,会影响原来的
//var a []int = make([]int, 3, 5)
////a[1] = 99 //[0 99 0]
//test2(a)
//fmt.Println(a) // [99 0 0]
// 10 多纬切片 切片定义并初始
//var a []int = []int{2, 3, 4, 45}
//fmt.Println(a)
//fmt.Println(len(a))
//fmt.Println(cap(a))
//a = append(a, 55)
//fmt.Println(len(a))
//fmt.Println(cap(a))
//var a [][]string = [][]string{{"1", "3"}, {"o"}, {"5", "o", "99"}}
//fmt.Println(a)
var a [][]string = make([][]string, 3, 3)
fmt.Println(a[2])
a[2] = make([]string, 3, 3)
fmt.Println(a[2][0])
}
func test2(a []int) {
a[0] = 99
//fmt.Println(a) //[99 99 0]
a = append(a, 33)
a[0] = 88
fmt.Println(a)
}
可变长参数
package main
import "fmt"
// 可变长参数
func main() {
//test3("1", "lqz")
var a []string = []string{"lqqz", "pyy"}
test3(a...) // 相当于打散了传入
}
func test3(a ...string) {
fmt.Println(a)
fmt.Printf("%T", a)
fmt.Println(cap(a))
}
maps
package main
import "fmt"
//map
func main() {
//1 key -value 形式存储 定义一个map
//var userInfo map[int]string // 没有初始化,它是nil,但是打印出来不是nil
//fmt.Println(userInfo) //map[]
//if userInfo == nil {
// fmt.Println("asdfasdf")
//}
// 2 map 初始化 方式一
//var userInfo map[int]string = map[int]string{1: "lqz", 3: "pyy"}
//fmt.Println(userInfo)
//if userInfo == nil {
// fmt.Println("asdfasdf")
//}
// 2 map 初始化 方式二 make初始化
//var userInfo map[int]string = make(map[int]string)
//fmt.Println(userInfo) //map[]
//if userInfo == nil {
// fmt.Println("asdfasdf")
//}
// 3 初始化后才能取值赋值
//var userInfo map[int]string = make(map[int]string)
//var userInfo map[int]string
////fmt.Println(userInfo[1])
//userInfo[1] = "pyy"
//fmt.Println(userInfo)
// 以后所有的引用类型,都需要初始化才能用,值类型不需要,有默认值
//4 取值赋值
//var userInfo map[string]string = make(map[string]string)
//userInfo["age"] = "19"
//userInfo["name"] = "lqz"
//fmt.Println(userInfo)
//
//fmt.Println(userInfo["age"])
//// 取不存在的----》显示vaule值的默认值
//fmt.Println("--", userInfo["hobby"])
//
//// 如何判断一个key在不在map中 按key取值,能返回一个布尔值,根据布尔值判断
//v, ok := userInfo["name"]
//fmt.Println(v)
//fmt.Println(ok) //false
//
//if v, ok := userInfo["name"]; ok {
// fmt.Println(v)
//}
// 5 删除map元素
//var userInfo map[string]string = make(map[string]string)
//userInfo["age"] = "19"
//userInfo["name"] = "lqz"
//delete(userInfo, "name")
//fmt.Println(userInfo)
// 6 map的长度
//var userInfo map[string]string = make(map[string]string)
//fmt.Println(len(userInfo))
//userInfo["age"] = "19"
//userInfo["name"] = "lqz"
//fmt.Println(len(userInfo))
// 7 引用类型
var userInfo map[string]string = make(map[string]string)
fmt.Println(len(userInfo))
userInfo["age"] = "19"
userInfo["name"] = "lqz"
test6(userInfo)
fmt.Println(userInfo)
}
func test6(u map[string]string) {
u["name"] = "pyy"
fmt.Println(u)
}
字符串
package main
// 字符串
func main() {
// 1 定义字符串
//var s = "中alqz"
// 2 字符串可以按下标取值,不能改
//s[0]=98
//fmt.Println(s[0]) // 取字节,是个数字
//fmt.Println(s[3])
//fmt.Printf("%T\n", s[3]) //uint8 类型 byte 取字符才是int32也就是rune
// 3 循环字符串
//for _, v := range s {
// fmt.Println(string(v)) //字符
//}
//for i := 0; i < len(s); i++ {
// fmt.Println(string(s[i])) // 字节
//}
// 4 字符串长度
//fmt.Println(len(s)) // 7 字节长度
//fmt.Println(utf8.RuneCountInString(s)) // 5 个字符
// 5 字符串是由字节或字符切片构成的
// 5.1 字符串和字节切片相互转化
//var s []byte = []byte{99, 97, 98, 'a'}
//fmt.Println(s)
// 字节切片,转成字符串
//fmt.Println(string(s))
//s := "abac"
// 把字符串转成字节切片
//var sss []byte = []byte(s)
//fmt.Println(sss)
//5.2 字符切片和字符串相互转化
//var s []rune = []rune{99, 97, 98, 'a', '你', '好', 20013}
//fmt.Println(string(s))
//s := "lqz中国"
//fmt.Println([]rune(s)) //22269
//s := "你好中国"
//fmt.Println(s)
//r := []rune(s)
//r[0] = 22269
//fmt.Println(string(r))
}
指针
# 指针:指针是一种存储变量内存地址的【变量】
# 指针规定 按照这个肯定每错
1 定义变量时,类型前加 * *int *string *[3]int ,表示类型,指向这个类型的指针类型
2 在变量前加 & 表示取这个变量的地址
3 在指针变量前加 * ,表示解引用,反解出内存地址指向的值
package main
import "fmt"
func main() {
// 指针是引用类型
// 1 定义指针
//var p *int
//fmt.Println(p) //<nil>
// 2 定义并赋初值
//var a int = 10
//var p *int = &a
//fmt.Println(p)
// 3 指针的解引用 ---》
//fmt.Println(*p)
// 4 取指针变量的地址
//var a int = 10
//var p *int = &a
//var p1 **int = &p // 取p的地址
//fmt.Println(p)
//fmt.Println(p1)
//var p2 ***int = &p1
//fmt.Println(p2)
//// 解引用
////fmt.Println(*p2)
//fmt.Println(**p2)
//fmt.Println(***p2)
// 5 向函数传递指针类型参数
//var a int = 10
////test1(a)
////fmt.Println(a)
//var p *int = &a
//test2(p)
//fmt.Println(a)
// 6 不要向函数传递 【数组的指针】,而应该使用切片
//var a [3]int
//var p *[3]int = &a
//fmt.Println(p) // &[0 0 0]
//test4(p)
//fmt.Println(a)
////test5(p)
//
//test6(a[:])
// 7 数组指针:数组的指针 指针数组: 数组里面的值是指针类型
// 数组的指针
//var a=[3]int{4,5,6}
//var p *[3]int = &a
//var p *[3]int = &[3]int{4, 5, 6}
//fmt.Println(p)
//指针数组
//var p1 [3]*int
//var a, b, c int = 9, 4, 6
//p1[0] = &a
//p1[1] = &b
//p1[2] = &c
//fmt.Println(p1)
//fmt.Println(*(p1[0]))
// 8 指针不允许运算
//var p *[3]int = &[3]int{4, 5, 6}
//fmt.Println(p)
//fmt.Println(p++)
}
func test1(a int) {
a = 99
fmt.Println(a)
}
func test2(p *int) {
*p = 99
fmt.Println(*p)
}
func test3(a [3]int) {
a[0] = 99
fmt.Println(a)
}
func test4(a *[3]int) {
//(*a)[0] = 99
a[0] = 99 // 可以简写成,等同于上面
fmt.Println(a)
}
func test5(a *[4]int) {
//(*a)[0] = 99
a[0] = 99 // 可以简写成,等同于上面
fmt.Println(a)
}
func test6(a []int) {
a[0] = 99
fmt.Println(a)
}
结构体
# go 的面向对象,go中没有面向对象的语法(class),但有面向对象概念(继承封装多态)
#什么是结构体(就是类的概念:类是若干方法和属性的集合)
结构体是用户定义的【类型】,表示若干个字段(Field)的集合
package main
import (
"fmt"
)
// 1 定义一个结构体体,定义个人结构体
type Person struct {
name string
age int
address string
hobby Hobby
}
//type Hobby struct {
// hobbyName string
// hobbyId int
//}
// 9 匿名字段
type Hobby struct {
string
int
}
func main() {
// 2 使用结构体。定义结构体变量
//var p Person = Person{name: "lqz", age: 19, address: "上海"}
// 3 结构体属性取值,赋值
//p.name = "彭于晏"
//fmt.Println(p)
//fmt.Println(p.age)
// 4 结构体是值类型--->不需要初始化,就有默认值,当参数传入函数中,修改,不会影响原来的,想修改原来的,要取地址
//var p Person
//p.name = "lqz"
//fmt.Println(p.name)
// 5 匿名结构体 ---》定义在函数内部,结构体没名字---》只用一次 --->没有名字,没有type就是匿名结构体
//var cat = struct {
// name string
// age int
//}{age: 19}
//fmt.Println(cat.name)
//fmt.Println(cat.age)
// 6 结构体初始化
//var p Person
//var p Person = Person{name: "lqz", age: 19, address: "上海"}
//var p Person = Person{name: "lqz"} // 没传的就是默认值
//var p Person = Person{"lqz",19,"上海"}
//var p Person = Person{"lqz"}
//var p Person = Person{}
// 7 结构体零值 ---》值类型,有默认值,不初始化也行
//var p Person
////fmt.Println(p)
////test7(p)
////fmt.Println(p)
//test8(&p)
//fmt.Println(p)
// 8 通过 . 访问和修改,结构体字段 (在包内,一定要导出)
//d := entity.Dog{Name: "小狗"}
//fmt.Println(d.Name)
//fmt.Println(d.Age)
// 9 匿名字段 初始化 匿名字段,类型名就是字段名
//var h Hobby = Hobby{"篮球", 100}
//var h Hobby = Hobby{string: "篮球", int: 19}
//fmt.Println(h)
//fmt.Println(h.string)
//fmt.Println(h.int)
// 10 结构体嵌套
//var p =Person{"lqz",19,"上海",Hobby{"篮球",100}}
//var p = Person{name: "lqz", age: 19, hobby: Hobby{string: "乒乓球", int: 1002}}
//fmt.Println(p.name)
//fmt.Println(p.hobby.string)
// 11 结构体嵌套+匿名字段,玩点高级的---》面向对象的继承
type Animal struct {
age int
name string
}
type Dog struct {
name string
Animal // 结构体嵌套+匿名字段 提升字段,Animal中的字段,可以直接在dog对象中使用---》相当于Dog继承了Animal,dog可以直接使用Animal的属性
}
//var d = Dog{"小野狗", Animal{age: 19}}
//fmt.Println(d.name)
//fmt.Println(d.Animal.age)
//// 提升字段
//fmt.Println(d.age)
//d.age = 22
//fmt.Println(d.Animal.age)
var d = Dog{"小野狗", Animal{age: 19, name: "动物"}} // 提升字段,有冲突,用自己的,如果要取到父类的,需要指名道姓
fmt.Println(d.name)
fmt.Println(d.Animal.name)
}
func test7(p Person) {
p.name = "xxx"
fmt.Println(p)
}
func test8(p *Person) {
//(*p).name = "xxx"
p.name = "xxx" // 简写成
fmt.Println(p)
}
方法
# 方法和函数? 方法可以自动传值
# 方法其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的
# 方法是绑定给结构体的,结构体有了一系列字段,又有了方法 ,那他就是类的概念了
package main
import "fmt"
// 方法
type Dog struct {
name string
age int
Wife
}
type Wife struct {
wifeName string
}
func (w Wife) printName() {
fmt.Println(w.wifeName)
}
func (d Dog) printName() {
fmt.Println(d.name)
}
// 给结构体绑定一个方法
func (d Dog) speak() {
fmt.Println(d.name, "在旺旺叫")
}
func (d Dog) run() {
fmt.Println(d.name, "在走路")
}
// 值类型接收器
//func (d Dog) changeName(name string) {
// d.name = name
// fmt.Println(d) // 改了
//}
// 指针类型接收器
func (d *Dog) changeName(name string) {
d.name = name
fmt.Println(d) // 改了
}
// 函数
//
// func speak(d Dog) {
// fmt.Println(d.name, "在旺旺叫")
//
// }
//func main() {
// 1 使用方法
//var dog = Dog{"小榔头", 12}
//dog.speak()
//dog.run()
// 2 为什么有了函数还要方法 方法是绑定给结构体的,可以自动传值,函数有几个值就要传几个值
//speak(dog)
//dog.speak()
// 3 值类型接收器和指针类型接收器
//var dog = Dog{"小榔头", 12}
//dog.changeName("小狼狗")
//fmt.Println(dog.name)
// 4 想修改外部的对象使用指针类型接收器,如果值很占内存,也可以使用指针类型接收器----》python中的self其实就是指针
// 5 匿名字段的方法,同理,也会提升
//var dog = Dog{"小榔头", 18, Wife{"小母狗"}}
//dog.printName()
//dog.Wife.printName()
// 6 在方法中使用值接收器 与 在函数中使用值参数
//var cat =Cat{"小野猫"}
//cat.speak()
//speak(cat)
// 使用指针
//var cat *Cat = &Cat{"小野猫"}
//cat.speak() // 值可以调用,指针也可以调用
////speak(cat) // 函数,参数是什么类型,就必须传什么类型
//cat.changeName("xx") // 方法中能不能改值,跟谁调用没关系,跟接收器类型有关系,只要是指针类型接收器,都能改,只要是值类型接收器,都不改
//fmt.Println(cat)
// 7 在方法中使用指针接收器 与 在函数中使用指针参数
//var cat = Cat{"小野猫"}
//cat.changeName("xxx")
//fmt.Println(cat)
//changeName(&cat,"xxx")
//var cat = &Cat{"小野猫"}
//cat.changeName("xxx")
//fmt.Println(cat)
//changeName(cat, "xxx")
// 1 方法可以值来调,也可以指针来调用
// 2 如果想改结构体对象的属性,必须用指针类型接收器来改
// 8 非结构体上的方法
//}
// 6 在方法中使用值接收器 与 在函数中使用值参数
//type Cat struct {
// name string
//}
//
//func (c Cat) speak() {
// fmt.Println(c.name, "喵喵叫")
//}
//func (c Cat) changeName(name string) {
// c.name = name
//}
//func speak(c Cat) {
// fmt.Println(c.name, "喵喵叫")
//}
// 7 在方法中使用指针接收器 与 在函数中使用指针参数
type Cat struct {
name string
}
func (c *Cat) changeName(name string) {
c.name = name
}
func changeName(c *Cat, name string) {
c.name = name
}
// 8 非结构体上的方法 int 内置的类型是不能绑定方法的,但是自定义的类型,可以绑定方法
//var a int =10
//a.Add()
//fmt.Println(a) // 11
type MyInt int
func (i *MyInt) Add() {
(*i)++
}
func main() {
var i MyInt = 10
i.Add()
i.Add()
i.Add()
i.Add()
fmt.Println(i)
}
接口
# 在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
class Animal:
def sepak():
pass
def run():
pass
class Dog(Animal):
def sepak():
print('ss')
def run():
print('ss')
class Cat(Animal):
def sepak():
print('ss')
def run():
print('ss')
#在 Go 语言中,接口就是方法的集合。当一个类型实现了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法
# GO也是鸭子类型
package main
import (
"fmt"
)
// 接口
// 1 定义一个接口
type Duck interface {
speak() // 没有具体实现,只是声明
run()
}
// 2 写一个结构体,实现接口
type PDuck struct {
age int
}
func (d PDuck) speak() {
fmt.Println("普通鸭子嘎嘎叫")
}
func (d PDuck) run() {
fmt.Println("普通鸭子歪歪扭扭走路")
}
type TDuck struct {
name string
age int
wife string
}
func (d TDuck) speak() {
fmt.Println("唐老鸭说人话")
}
func (d TDuck) run() {
fmt.Println("唐老鸭人走路")
}
func main() {
// 1 多态:不考虑对象具体类型的情况下使用对象,把对象当成接口类型,只要实现了接口,就直接使用
//var p = PDuck{1}
//var t = TDuck{"唐老鸭", 13, "女唐老鸭"}
//test9(p)
//test9(t)
// 2 接口也是一种类型
//var ddd Duck
//ddd = PDuck{1}
//ddd = TDuck{"唐老鸭", 13, "女唐老鸭"}
//fmt.Println(ddd)
// 3 一旦把具体对象赋值给接口类型---》只能使用接口中的方法, 其他方法或属性,都取出出来了
//var ddd Duck
//ddd = TDuck{"唐老鸭", 13, "女唐老鸭"}
//fmt.Println(ddd.name)
// 4 把接口类型, 转换成具体类型 类型断言
//var ddd Duck
//var t = TDuck{"唐老鸭", 13, "女唐老鸭"}
////var t = ddd.(TDuck) //ddd接口类型.(具体类型) ---》把这个接口类型转成具体类型
////fmt.Println(t.age)
//test9(t)
// 5 直接强转可能会报错,类型选择
//var t = TDuck{"唐老鸭", 13, "女唐老鸭"}
//var p = PDuck{1}
//test10(p)
// 6 空接口类型 所有类型都实现了空接口
//var a int =10
//var t = TDuck{"唐老鸭", 13, "女唐老鸭"}
//test11(a)
//test11("lqz")
//test11(t)
// 7 匿名空接口
fmt.Println()
}
// func test9(p Duck) {
// p.speak()
// // p 到底是普通鸭子呢,还是唐老鸭
// //var t = p.(PDuck) // 会报错
// var t = p.(TDuck) // 会报错
// fmt.Println(t.name)
// }
//func test10(p Duck) {
// switch v := p.(type) {
// case PDuck:
// fmt.Println(v.age)
// case TDuck:
// fmt.Println(v.name)
// }
//}
//type Empty interface {
//}
//
//func test11(p Empty) {
// fmt.Println(p)
//}
// 匿名,空接口
func test12(p interface{}) {
}
标签:Println,string,name,int,fmt,var,maps,go,指针
From: https://www.cnblogs.com/xm15/p/17362901.html