学习视频: 8 小时转职 Golang 工程师,这门课很适合有一定开发经验的小伙伴,强推!
Golang 语言特性
Golang 的优势
极简单的部署方式:可直接编译成机器码、不依赖其他库、直接运行即可部署。
静态类型语言,编译的时候可以检查出大多数问题。
语言层面的并发:天生的基因支持、充分的利用多核
// Go 语言实现并发的代码
func goFunc(i int) {
fmt.Println("goroutine ", i, " ...")
}
func main() {
for i := 0; i < 1000; i++ {
go goFunc(i) // 开启一个并发协程
}
time.Sleep(time.Second)
}
强大的标准库:runtime 系统调度机制、高效的 CG 垃圾回收、丰富的标准库
“大厂” 领军:Google、facebook、Tencent、Baidu、七牛、字节 ......
不同语言的斐波那契数列算法 编译 + 运行 时间对比:
Golang 的应用场景
1、云计算基础设施领域:
代表项目:docker、kubernetes、etcd、consul、cloud flare CDN、七牛云存储 等。
2、基础后端软件:
代表项目:tidb、influxdb、 cockroach 等。
3、微服务
代表项目:go-kit、 micro、 monzo bank 的 typhon、bilibili 等。
4、互联网基础设施
代表项目:以太坊、hyperledger 等。
Golang 明星作品:Docker、Kubernetes
Golang 的不足
1、包管理,大部分包都托管在 Github 上。
像我们熟悉的 maven、npm 等都有专门的包管理组织;托管在 Github 上的代码容易被作者个人操作影响到使用该项目的工程。
2、无泛化类型。
据说很快就会有了。
3、所有 Exception 都用 Error 来处理(有争议)。
4、对 C 的降级处理,并非无缝,没有 C 降级到 asm 那么完美。(序列化问题)
基础语法
main
package main
// 导多个包
import (
"fmt"
"time"
)
// Golang 有无 ; 都可,建议不加
func main() { // 函数的 { 必须和函数名同行, 否则编译报错
fmt.Println("Hello Go!")
time.Sleep(1 * time.Second)
}
变量
输出变量类型的方法:
var a int fmt.Printf("type of a = %T\n", a) // type of a = int
局部变量的声明:
// 方法一:声明一个变量 默认的值是0
var a int
// 方法二:声明一个变量,初始化一个值
var b int = 100
// 方法三:初始化的时候, 省去数据类型,通过值自动匹配当前的变量的数据类型
var c = 100
// 方法四:(常用) 省去var关键字,直接自动匹配
d := 100
全局变量的声明:以上只有方法四不支持(编译会报错)
多变量的声明:
// 单行写法
var xx, yy int = 100, 200
var kk, ll = 100, "Aceld"
// 多行写法
var (
vv int = 100
jj bool = true
)
常量与 iota
使用 const 定义常量,常量是只读的,不允许修改。
const a int = 10
const (
a = 10
b = 20
)
const 可以用来定义枚举:
const {
BEIJING = 0
SHANGHAI = 1
SHENZHEN = 3
}
const 可以和 ==iota== 一起使用来定义有规则的枚举:
const (
// 可以在const() 添加一个关键字 iota, 每行的iota都会累加1, 第一行的iota的默认值是0
BEIJING = iota // iota = 0
SHANGHAI // iota = 1
SHENZHEN // iota = 2
)
const (
a, b = iota+1, iota+2 // iota = 0, a = iota + 1, b = iota + 2, a = 1, b = 2
c, d // iota = 1, c = iota + 1, d = iota + 2, c = 2, d = 3
e, f // iota = 2, e = iota + 1, f = iota + 2, e = 3, f = 4
g, h = iota * 2, iota *3 // iota = 3, g = iota * 2, h = iota * 3, g = 6, h = 9
i, k // iota = 4, i = iota * 2, k = iota * 3 , i = 8, k = 12
)
string
对于字符串操作的 4 个包:bytes、strings、strconv、unicode
- bytes 包操作
[]byte
。因为字符串是只读的,因此逐步构创建字符串会导致很多分配和复制,使用bytes.Buffer
类型会更高。 - strings 包提供 切割、索引、前缀、查找、替换 等功能。
- strconv 包提供 布尔型、整型数、浮点数 和对应字符串的相互转换,还提供了双引号转义相关的转换。
- unicode 包提供了 IsDigit、IsLetter、IsUpper、IsLower 等类似功能,用于给字符分类。
如果 string 中包含汉字,要注意:
- UTF-8 编码中,一个汉字需要 3 个字节,通过
len()
获取的是字符串占据的字节数。
str1 := "hello 世界"
fmt.Println(len(str1)) // 12
- 如果想要得到字符串本身的长度,可以将 string 转为 rune 数组再计算:
str2 := "hello 世界"
fmt.Println(len([]rune(str2))) // 8
字符串遍历
byte 是 uint8 的别名rune 是 int32 的别名,相当于 Go 里面的 char
如果包含汉字,以下遍历方式会出现乱码:
str := "你好世界!"
for i := 0; i < len(str); i++ {
fmt.Printf("%c", str[i])
}
// ä½ å¥½ä¸çï¼%
- 解决方案1:转成 rune 切片再遍历
str := "你好世界!"
newStr := []rune(str)
for i := 0; i < len(newStr); i++ {
fmt.Printf("%c", newStr[i])
}
// 你好世界!
- 解决方案2:使用 range 来遍历
range 按照字符遍历,前面的 for 按照字节遍历
str := "你好世界123"
for index, value := range str {
fmt.Printf("index = %d value = %c\n", index, value)
}
index = 0 value = 你
index = 3 value = 好
index = 6 value = 世
index = 9 value = 界
index = 12 value = 1
index = 13 value = 2
index = 14 value = 3
strings 包
字符串比较:使用 strings.Compare
比较两个字符串的字典序
strings.Compare("aaa", "bbb") // -1
strings.Compare("baa", "abb") // 1
strings.Compare("aaa", "aaa") // 0
查找函数:使用 strings.Index
查找字符串中子串的位置(第 1 个),不存在返回 -1
strings.Index("hello world", "o") // 4
类似的,使用 strings.LastIndex
查找字符串子串出现的最后一个位置,不存在返回 -1
strings.Index("hello world", "o") // 4
Count、Repeat:
使用 strings.Count
统计子串在整体中出现的次数:
strings.Count("abc abc abab abc", "abc") // 3
使用 strings.Repeat
将字符串重复指定次数:
strings.Repeat("abc", 3) // abcabcabc
Replace、Split、Join:
strings.Replace
实现字符串替换
str := "acaacccc"
// 局部替换 param3: 替换次数,< 0 则全部替换
strings.Replace(str, "a", "b", 2) // bcbacccc
strings.Replace(str, "a", "b", -1) // bcbbcccc
// 全部替换
strings.ReplaceAll(str, "a", "b") // bcbbcccc
strings.Split
实现字符串切割
str := "abc,bbc,bbd"
slice := strings.Split(str, ",")
fmt.Println(slice) // [abc bbc bbd]
strings.Join
实现字符串拼接
slice := []string{"aab", "aba", "baa"}
str := strings.Join(slice, ",")
fmt.Println(str // aab,aba,baa
bytes 包
Buffer 是 bytes 包中定义的 type Buffer struct {...}
,Bufer 是一个变长的可读可写的缓冲区。
创建缓冲器:bytes.NewBufferString
、bytes.NewBuffer
func main() {
buf1 := bytes.NewBufferString("hello")
buf2 := bytes.NewBuffer([]byte("hello"))
buf3 := bytes.NewBuffer([]byte{'h', 'e', 'l', 'l', 'o'})
fmt.Printf("%v,%v,%v\n", buf1, buf2, buf3)
fmt.Printf("%v,%v,%v\n", buf1.Bytes(), buf2.Bytes(), buf3.Bytes())
buf4 := bytes.NewBufferString("")
buf5 := bytes.NewBuffer([]byte{})
fmt.Println(buf4.Bytes(), buf5.Bytes())
}
hello,hello,hello
[104 101 108 108 111],[104 101 108 108 111],[104 101 108 108 111]
[] []
写入缓冲器:Write
、WriteString
、WriteByte
、WriteRune
、WriteTo
func main() {
buf := bytes.NewBufferString("a")
fmt.Printf("%v, %v\n", buf.String(), buf.Bytes())
// a, [97]
buf.Write([]byte("b")) // Write
buf.WriteString("c") // WriteString
buf.WriteByte('d') // WriteByte
buf.WriteRune('e') // WriteRune
fmt.Printf("%v, %v\n", buf.String(), buf.Bytes())
// abcde, [97 98 99 100 101]
}
缓冲区原理介绍:Go 字节缓冲区底层以字节切片做存储,切片存在长度 len 与容量 cap
- 缓冲区从长度 len 的位置开始写,当 len > cap 时,会自动扩容
- 缓冲区从内置标记 off 位置开始读(off 始终记录读的起始位置)
- 当 off == len 时,表明缓冲区已读完,读完就重置缓冲区 len = off = 0
func main() {
byteSlice := make([]byte, 20)
byteSlice[0] = 1 // 将缓冲区第一个字节置1
byteBuffer := bytes.NewBuffer(byteSlice) // 创建20字节缓冲区 len = 20 off = 0
c, _ := byteBuffer.ReadByte() // off+=1
fmt.Printf("len:%d, c=%d\n", byteBuffer.Len(), c) // len = 20 off =1 打印c=1
byteBuffer.Reset() // len = 0 off = 0
fmt.Printf("len:%d\n", byteBuffer.Len()) // 打印len=0
byteBuffer.Write([]byte("hello byte buffer")) // 写缓冲区 len+=17
fmt.Printf("len:%d\n", byteBuffer.Len()) // 打印len=17
byteBuffer.Next(4) // 跳过4个字节 off+=4
c, _ = byteBuffer.ReadByte() // 读第5个字节 off+=1
fmt.Printf("第5个字节:%d\n", c) // 打印:111(对应字母o) len=17 off=5
byteBuffer.Truncate(3) // 将未字节数置为3 len=off+3=8 off=5
fmt.Printf("len:%d\n", byteBuffer.Len()) // 打印len=3为未读字节数 上面len=8是底层切片长度
byteBuffer.WriteByte(96) // len+=1=9 将y改成A
byteBuffer.Next(3) // len=9 off+=3=8
c, _ = byteBuffer.ReadByte() // off+=1=9 c=96
fmt.Printf("第9个字节:%d\n", c) // 打印:96
}
缓冲区:
func main() {
buf := &bytes.Buffer{}
// 写缓冲区
buf.WriteString("abc?def")
// 从缓冲区读(分隔符为 ?)
str, _ := buf.ReadString('?')
fmt.Println("str = ", str)
fmt.Println("buff = ", buf.String())
}
str = abc?
buff = def
缓冲区读数据:Read
、ReadByte
、ReadByes
、ReadString
、ReadRune
、ReadFrom
func main() {
log.SetFlags(log.Lshortfile)
buff := bytes.NewBufferString("123456789")
log.Println("buff = ", buff.String()) // buff = 123456789
// 从缓冲区读取4个字节
s := make([]byte, 4)
n, _ := buff.Read(s)
log.Println("buff = ", buff.String()) // buff = 56789
log.Println("s = ", string(s)) // s = 1234
log.Println("n = ", n) // n = 4
// 从缓冲区读取4个字节
n, _ = buff.Read(s)
log.Println("buff = ", buff.String()) // buff = 9
log.Println("s = ", string(s)) // s = 5678
log.Println("n = ", n) // n = 4
n, _ = buff.Read(s)
log.Println("buff = ", buff.String()) // buff =
log.Println("s = ", string(s)) // s = 9678
log.Println("n = ", n) // n = 1
buff.Reset()
buff.WriteString("abcdefg")
log.Println("buff = ", buff.String()) // buff = abcdefg
b, _ := buff.ReadByte()
log.Println("b = ", string(b)) // b = a
log.Println("buff = ", buff.String()) // buff = bcdefg
b, _ = buff.ReadByte()
log.Println("b = ", string(b)) // b = b
log.Println("buff = ", buff.String()) // buff = cdefg
bs, _ := buff.ReadBytes('e')
log.Println("bs = ", string(bs)) // bs = cde
log.Println("buff = ", buff.String()) // buff = fg
buff.Reset()
buff.WriteString("编译输出GO")
r, l, _ := buff.ReadRune()
log.Println("r = ", r, ", l = ", l, ", string(r) = ", string(r))
// r = 32534 , l = 3 , string(r) = 编
buff.Reset()
buff.WriteString("qwer")
str, _ := buff.ReadString('?')
log.Println("str = ", str) // str = qwer
log.Println("buff = ", buff.String()) // buff =
buff.WriteString("qwer")
str, _ = buff.ReadString('w')
log.Println("str = ", str) // str = qw
log.Println("buff = ", buff.String()) // buff = er
file, _ := os.Open("doc.go")
buff.Reset()
buff.ReadFrom(file)
log.Println("doc.go = ", buff.String()) // doc.go = 123
buff.Reset()
buff.WriteString("中国人")
cbyte := buff.Bytes()
log.Println("cbyte = ", cbyte) // cbyte = [228 184 173 229 155 189 228 186 186]
}
strconv 包
字符串转 []byte:
sum := []byte("hello")
字符串 ---> 整数:使用 strconv.Atoi
或 strconv.ParseInt
// 按照 10进制 转换,返回 int 类型
i, _ := strconv.Atoi("33234")
fmt.Printf("%T\n", i) // int
// param1:要转化的字符串
// param2:转换的进制,如 2,8,16,32
// param3:返回bit的大小(注意,字面量显示还是 int64)
i2, _ := strconv.ParseInt("33234", 10, 0)
fmt.Printf("%T\n", i2) // int64
字符串 ---> 浮点数:使用 strconv.ParseFloat
// 参数类似 ParseInt
val, _ := strconv.ParseFloat("33.33", 32)
fmt.Printf("type: %T\n", val) // type: float64
val2, _ := strconv.ParseFloat("33.33", 64)
fmt.Printf("type: %T\n", val2) // type: float64
整数 ---> 字符串:使用 strconv.Iota
或 strconv.FormatInt
num := 180
// 默认按照10进制转换
f1 := strconv.Itoa(num)
// param1: 要转换的数字(必须是int64类型)
// param2: 转换的进制
f2 := strconv.FormatInt(int64(num), 10)
浮点数 ---> 整数:使用 strconv.FormatFloat
num := 23423134.323422
fmt.Println(strconv.FormatFloat(float64(num), 'f', -1, 64)) // 普通模式
fmt.Println(strconv.FormatFloat(float64(num), 'b', -1, 64)) // 二进制模式
fmt.Println(strconv.FormatFloat(float64(num), 'e', -1, 64)) // 科学记数法
fmt.Println(strconv.FormatFloat(float64(num), 'E', -1, 64)) // 同上,显示为E
fmt.Println(strconv.FormatFloat(float64(num), 'g', -1, 64)) // 指数大时用科学记数,否则普通模式
fmt.Println(strconv.FormatFloat(float64(num), 'G', -1, 64)) // 同上,显示为E
23423134.323422
6287599743057036p-28
2.3423134323422e+07
2.3423134323422E+07
2.3423134323422e+07
2.3423134323422E+07
字符串 和 bool 类型转换:
// string --> bool
flagBool, _ := strconv.ParseBool("true")
// It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False.
// Any other value returns an error.
// bool --> string
flagStr := strconv.FormatBool(true)
unicode 包
/src/unicode/letter.go
:
// 判断字符 r 是否为大写格式
func IsUpper(r rune) bool
// 判断字符 r 是否为小写格式
func IsLower(r rune) bool
// 判断字符 r 是否为 Unicode 规定的 Title 字符
// 大部分字符的 Title 格式就是其大写格式
// 只有少数字符的 Title 格式是特殊字符
// 这里判断的就是特殊字符
func IsTitle(r rune) bool
// ToUpper 将字符 r 转换为大写格式
func ToUpper(r rune) rune
// ToLower 将字符 r 转换为小写格式
func ToLower(r rune) rune
// ToTitle 将字符 r 转换为 Title 格式
// 大部分字符的 Title 格式就是其大写格式
// 只有少数字符的 Title 格式是特殊字符
func ToTitle(r rune) rune
// To 将字符 r 转换为指定的格式
// _case 取值:UpperCase、LowerCase、TitleCase
func To(_case int, r rune) rune
/src/unicode/digit.go
:
// IsDigit 判断 r 是否为一个十进制的数字字符
func IsDigit(r rune) bool
/src/unicode/graphic.go
:
// IsNumber 判断 r 是否为一个数字字符 (类别 N)
func IsNumber(r rune) bool
// IsLetter 判断 r 是否为一个字母字符 (类别 L)
// 汉字也是一个字母字符
func IsLetter(r rune) bool
// IsSpace 判断 r 是否为一个空白字符
// 在 Latin-1 字符集中,空白字符为:\t, \n, \v, \f, \r,
// 空格, U+0085 (NEL), U+00A0 (NBSP)
// 其它空白字符的定义有“类别 Z”和“Pattern_White_Space 属性”
func IsSpace(r rune) bool
// IsControl 判断 r 是否为一个控制字符
// Unicode 类别 C 包含更多字符,比如代理字符
// 使用 Is(C, r) 来测试它们
func IsControl(r rune) bool
// IsGraphic 判断字符 r 是否为一个“图形字符”
// “图形字符”包括字母、标记、数字、标点、符号、空格
// 他们分别对应于 L、M、N、P、S、Zs 类别
// 这些类别是 RangeTable 类型,存储了相应类别的字符范围
func IsGraphic(r rune) bool
// IsPrint 判断字符 r 是否为 Go 所定义的“可打印字符”
// “可打印字符”包括字母、标记、数字、标点、符号和 ASCII 空格
// 他们分别对应于 L, M, N, P, S 类别和 ASCII 空格
// “可打印字符”和“图形字符”基本是相同的,不同之处在于
// “可打印字符”只包含 Zs 类别中的 ASCII 空格(U+0020)
func IsPrint(r rune) bool
// IsPunct 判断 r 是否为一个标点字符 (类别 P)
func IsPunct(r rune) bool
// IsSymbol 判断 r 是否为一个符号字符
func IsSymbol(r rune) bool
// IsMark 判断 r 是否为一个 mark 字符 (类别 M)
func IsMark(r rune) bool
// IsOneOf 判断 r 是否在 set 范围内
func IsOneOf(set []*RangeTable, r rune) bool
循环语句
go 语言中的 for 循环有 3 种形式:
for init; condition; post { }
for condition { }
for { }
func main() {
numbers := [6]int{1, 2, 3, 5}
for i := 0; i < len(numbers); i++ {
fmt.Println(numbers[i])
}
i := 0
for i < len(numbers) {
fmt.Println(numbers[i])
i++
}
for i, x := range numbers {
fmt.Printf("index: %d, value: %d\n", i, x)
}
// 无限循环
for {
fmt.Println("endless...")
}
}
range
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
// 忽略value, 只取index, 支持 string/array/slice/map
for i := range numbers {
fmt.Println(numbers[i])
}
// 忽略 index
for _, n := range numbers {
fmt.Println(n)
}
// 忽略全部返回值,仅迭代
for range numbers {
}
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v)
}
}
注意:range 会复制对象
func main() {
a := [3]int{0, 1, 2}
for i, v := range a { // index, value 都是从复制品中取出
if i == 0 { // 修改前,先修改原数组
a[1], a[2] = 999, 999
fmt.Println(a) // 确认修改有效 [0, 999, 999]
}
a[i] = v + 100 // 使用复制品中取出的 value 修改原数组
}
fmt.Println(a) // [100, 101, 102]
}
函数
多返回值
单返回值的函数:
func foo1(a string, b int) int {
return 100
}
多返回值的函数:
// 返回多个返回值,匿名的
func foo2(a string, b int) (int, int) {
return 666, 777
}
// 返回多个返回值,有形参名称的
func foo3(a string, b int) (r1 int, r2 int) {
// r1 r2 属于foo3的形参,初始化默认的值是0
// r1 r2 作用域空间 是foo3 整个函数体的{}空间
fmt.Println("r1 = ", r1) // 0
fmt.Println("r2 = ", r2) // 0
// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
return
}
func foo4(a string, b int) (r1, r2 int) {
// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
return
}
init 函数
每个 go 程序都会在一开始执行 init() 函数,可以用来做一些初始化操作:
package main
import "fmt"
func init() {
fmt.Println("init...")
}
func main() {
fmt.Println("hello world!")
}
init...
hello world!
如果一个程序依赖了多个包,它的执行流程如下图:
制作包的时候,项目路径如下:
$GOPATH/GolangStudy/5-init/
├── lib1/
│ └── lib1.go
├── lib2/
│ └── lib2.go
└── main.go
lib1 .init() ...
lib2 .init() ...
lib1Test()
lib2Test()
闭包
func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}
func main() {
c := a()
c() // 1
c() // 2
c() // 3
a() // 无输出
}
import 导包
import _ "fmt"
给 fmt 包一个匿名, ⽆法使用该包的⽅法,但是会执行该包内部的 init() 方法
import aa "fmt"
给 fmt 包起一个别名 aa,可以用别名直接调用:
aa.Println()
import . "fmt"
将 fmt 包中的全部方法,导入到当前包的作用域中,全部方法可以直接调用,无需
fmt.API
的形式
匿名函数
匿名函数的使用:
func main() {
res := func(n1 int, n2 int) int {
return n1 * n2
}(10, 20)
fmt.Printf("res: %v\n", res)
}
将匿名函数赋值给变量,通过变量调用:
func main() {
ret := func(n1 int, n2 int) int {
return n1 + n2
}
// 变量调用
sum := ret(100, 20)
fmt.Printf("sum: %v\n", sum)
// 多次调用
sum2 := ret(1000, 30)
fmt.Printf("sum2: %v\n", sum2)
}
指针
经典:在函数中交换两数的值
func swap(pa *int, pb *int) {
var temp int
temp = *pa
*pa = *pb
*pb = temp
}
func main() {
var a, b int = 10, 20
swap(&a, &b) // 传地址
fmt.Println("a = ", a, " b = ", b)
}
defer
defer 声明的语句会在当前函数执行完之后调用:
func main() {
defer fmt.Println("main end")
fmt.Println("main::hello go ")
}
main::hello go
main end
如果有多个 defer,依次入栈,函数返回后依次出栈执行:
上图执行顺序:func3() -> func2() -> func1()
关于 defer 和 return 谁先谁后:
func deferFunc() int {
fmt.Println("defer func called...")
return 0
}
func returnFunc() int {
fmt.Println("return func called...")
return 0
}
func returnAndDefer() int {
defer deferFunc()
return returnFunc()
}
func main() {
returnAndDefer()
}
return func called...
defer func called...
结论:return 之后的语句先执⾏,defer 后的语句后执⾏。
切片 slice
Golang 默认都是采用值传递,有些值天生就是指针:slice、map、channel。注意:定长数组是值传递,slice 是指针传递。
数组
声明数组的方式:(固定长度的数组)
var array1 [10]int
array2 := [10]int{1,2,3,4}
array3 := [4]int{1,2,3,4}
数组的长度是固定的,并且在传参的时候,严格匹配数组类型
// 传入参数的数组长度为4,则只能传递长度为4的数组
func printArray(myArray [4]int) {
fmt.Println(myArray) // [1 2 3 4]
myArray[0] = 666 // 数组是值传递
}
func main() {
myArray := [4]int{1, 2, 3, 4}
printArray(myArray)
fmt.Println(myArray) // [1 2 3 4]
}
myArray := [...]int{1, 2, 3, 4}
是自动计算数组长度,但并不是引用传递。
声明动态数组和声明数组一样,只是不用写长度。
// 不指定长度则是动态数组
func printArray(myArray []int) {
fmt.Println(myArray) // [1 2 3 4]
myArray[0] = 10 // 动态数组是引用传递
}
func main() {
myArray := []int{1, 2, 3, 4}
printArray(myArray)
fmt.Println(myArray) // [10 2 3 4]
}
slice
slice 的声明方式:通过 make 关键字
// 1 声明一个切片,并且初始化,默认值是1,2,3,长度是3
slice1 := []int{1, 2, 3} // [1 2 3]
// 2 声明一个切片,但是没有给它分配空间
var slice2 []int // slice2 == nil
// 开辟3个空间,默认值是0
slice2 = make([]int, 3) // [0 0 0]
// 3 声明一个切片,同时给slice分配3个空间,默认值是0
var slice3 []int = make([]int, 3) // [0 0 0]
// 4 声明一个切片,同时给slice分配3个空间,默认值是0,通过:=推导出slice是一个切片
slice4 := make([]int, 3) // [0 0 0]
len() 和 cap() 函数:
- len:长度,表示左指针⾄右指针之间的距离。
- cap:容量,表示指针至底层数组末尾的距离。
切⽚的扩容机制,append 的时候,如果长度增加后超过容量,则将容量增加 2 倍。
var numbers = make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 向numbers切片追加一个元素1, len = 4, [0,0,0,1], cap = 5
numbers = append(numbers, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 向numbers切片追加一个元素2, len = 5, [0,0,0,1,2], cap = 5
numbers = append(numbers, 2)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 向一个容量cap已经满的slice 追加元素, len = 6, cap = 10
numbers = append(numbers, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
len = 3, cap = 5, slice = [0 0 0]
len = 4, cap = 5, slice = [0 0 0 1]
len = 5, cap = 5, slice = [0 0 0 1 2]
len = 6, cap = 10, slice = [0 0 0 1 2 3]
slice 操作
slice 截取是浅拷贝,若想深拷贝需要使用 copy
可以通过设置下限以及上限设置截取切片 [lower-bound: upper-bound]
,实例:
func main() {
/* 创建切片 */
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(numbers)
/* 打印原始切片 */
fmt.Println("number ==", numbers)
/* 打印子切片从索引1(包含)到索引4(不包含) */
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0 */
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s) */
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int, 0, 5)
fmt.Println(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
numbers2 := numbers[:2]
fmt.Println(numbers2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
numbers3 := numbers[2:5]
fmt.Println(numbers3)
}
[0 1 2 3 4 5 6 7 8]
number == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
[]
[0 1]
[2 3 4]
利用 copy 函数拷贝切片,是深拷贝。
slice1 := []int{1, 2, 3}
slice2 := make([]int, 3)
copy(slice2, slice1)
slice2[0] = 10
fmt.Println(slice1) // [1 2 3]
直接赋值切片,是浅拷贝。
slice1 := []int{1, 2, 3}
slice2 := slice1
slice2[0] = 10
fmt.Println(slice1) // [10 2 3]
...
...
是 Go 的一种语法糖。
- 用法 1:函数可以用来接受多个不确定数量的参数。
- 用法 2:slice 可以被打散进行传递。
func test(args ...string) {
for _, v := range args {
fmt.Println(v)
}
}
func main() {
var ss = []string{
"abc",
"efg",
"hij",
"123",
}
test(ss...)
}
map
slice、map、channel 都是引用类型,声明后还需要初始化分配内存,即 make
map 的声明
map 的第一种声明方式:
// 声明myMap1是一种map类型 key是string,value是string
var myMap1 map[string]string
fmt.Println(myMap1 == nil) // true
// 使用map前,需要先用make给map分配数据空间
myMap1 = make(map[string]string, 10)
myMap1["one"] = "java"
myMap1["two"] = "c++"
myMap1["three"] = "python"
fmt.Println(myMap1)
// map[one:java three:python two:c++]
map 的第二种声明方式:
myMap2 := make(map[int]string)
myMap2[1] = "java"
myMap2[2] = "c++"
myMap2[3] = "python"
fmt.Println(myMap2)
// map[1:java 2:c++ 3:python]
map 的第三种声明方式:
myMap3 := map[string]string {
"one": "php",
"two": "c++",
"three": "python",
}
fmt.Println(myMap3)
// map[one:java three:python two:c++]
map 的使用
func printMap(cityMap map[string]string) {
for key, value := range cityMap {
fmt.Println("key = ", key+", value = ", value)
}
}
func AddValue(cityMap map[string]string) {
// map 是引用传递
cityMap["England"] = "London"
}
func main() {
cityMap := make(map[string]string)
// 添加
cityMap["China"] = "Beijing"
cityMap["Japan"] = "Tokyo"
cityMap["USA"] = "NewYork"
// 删除
delete(cityMap, "China")
// 遍历
printMap(cityMap)
fmt.Println("-------")
// 修改
cityMap["USA"] = "DC"
// 利用函数添加 - map 是引用传递
AddValue(cityMap)
// 遍历
printMap(cityMap)
}
key = Japan, value = Tokyo
key = USA, value = NewYork
-------
key = England, value = London
key = Japan, value = Tokyo
key = USA, value = DC
判断 map 中 key 值是否存在:直接取值,返回有两个返回值,通过第 2 个返回值判断。
m := make(map[string]interface{})
m["a"] = "AAA"
if _, ok := m["ba"]; ok {
fmt.Println("存在")
} else {
fmt.Println("不存在")
}
error
捕获系统抛出异常:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获:", err)
}
}()
nums := []int{1, 2, 3}
fmt.Println(nums[4]) // 系统抛出异常
// 捕获: runtime error: index out of range [4] with length 3
}
手动抛出异常并捕获:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获:", err)
}
}()
panic("出现异常!") // 手动抛出异常
// 捕获: 出现异常!
}
返回异常:
func getCircleArea(radius float32) (area float32, err error) {
if radius < 0 {
// 构建个异常对象
err = errors.New("半径不能为负")
return
}
area = 3.14 * radius * radius
return
}
func main() {
area, err := getCircleArea(-5)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(area)
}
}
自定义异常:
type PathError struct {
path string
op string
createTime string
message string
}
func (p *PathError) Error() string {
return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s",
p.path, p.op, p.createTime, p.message)
}
func Open(filename string) error {
file, err := os.Open(filename)
if err != nil {
return &PathError{
path: filename,
op: "read",
message: err.Error(),
createTime: fmt.Sprintf("%v", time.Now()),
}
}
defer file.Close()
return nil
}
func main() {
err := Open("test.txt")
switch v := err.(type) {
case *PathError:
fmt.Println("get path error,", v)
default:
}
}
面向对象编程
type
利用 type 可以声明某个类型的别名(理解为声明一种新的数据类型)
type myint int
func main() {
var a myint = 10
fmt.Println("a = ", a)
fmt.Printf("type of a = %T\n", a)
}
a = 10
type of a = main.myint
方法
方法:包含了接受者的函数,接受者可以是命名类型或结构体类型的值或者指针。
方法和普通函数的区别:
- 对于普通函数,参数为值类型时,不能将指针类型的数据直接传递,反之亦然。
- 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法(反过来也可以)。
// 1.普通函数
// 接收值类型参数的函数
func valueIntTest(a int) int {
return a + 10
}
// 接收指针类型参数的函数
func pointerIntTest(a *int) int {
return *a + 10
}
func structTestValue() {
a := 2
fmt.Println("valueIntTest:", valueIntTest(a))
// 函数的参数为值类型,则不能直接将指针作为参数传递
// fmt.Println("valueIntTest:", valueIntTest(&a))
// compile error: cannot use &a (type *int) as type int in function argument
b := 5
fmt.Println("pointerIntTest:", pointerIntTest(&b))
// 同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
// fmt.Println("pointerIntTest:", pointerIntTest(b))
// compile error:cannot use b (type int) as type *int in function argument
}
// 2.方法
type PersonD struct {
id int
name string
}
//接收者为值类型
func (p PersonD) valueShowName() {
fmt.Println(p.name)
}
//接收者为指针类型
func (p *PersonD) pointShowName() {
fmt.Println(p.name)
}
func structTestFunc() {
// 与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
// 值类型调用方法
personValue := PersonD{101, "hello world"}
personValue.valueShowName()
personValue.pointShowName()
// 指针类型调用方法
personPointer := &PersonD{102, "hello golang"}
personPointer.valueShowName()
personPointer.pointShowName()
}
struct
type Book struct {
title string
price string
}
func changeBook(book Book) {
// 传递一个book的副本
book.price = "666"
}
func changeBook2(book *Book) {
// 指针传递
book.price = "777"
}
func main() {
var book Book
book.title = "Golang"
book.price = "111"
fmt.Printf("%v\n", book) // {Golang 111}
changeBook(book)
fmt.Printf("%v\n", book) // {Golang 111}
changeBook2(&book)
fmt.Printf("%v\n", book) // {Golang 777}
}
一道 struct 与指针面试题:
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "aaa", age: 18},
{name: "bbb", age: 23},
{name: "ccc", age: 28},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
aaa => ccc
bbb => ccc
ccc => ccc
解决方法 1:
for _, stu := range stus {
// 方法1
temp := stu
m[stu.name] = &temp
}
解决方法 2:
for i, stu := range stus {
// 方法2
m[stu.name] = &stus[i]
}
封装
Golang 中,类名、属性名、⽅法名 首字⺟大写 表示对外(其他包)可以访问,否则只能够在本包内访问。
// 如果类名首字母大写,表示其他包也能够访问
type Hero struct {
// 如果类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
Name string
Ad int
level int // 只能本包访问
}
func (h *Hero) Show() {
fmt.Println("Name = ", h.Name)
fmt.Println("Ad = ", h.Ad)
fmt.Println("Level = ", h.level)
fmt.Println("---------")
}
func (h *Hero) GetName() string {
return h.Name
}
// 不用指针则传递的是副本,无法赋值
func (h *Hero) SetName(newName string) {
h.Name = newName
}
func main() {
hero := Hero{Name: "zhang3", Ad: 100}
hero.Show()
hero.SetName("li4")
hero.Show()
}
Name = zhang3
Ad = 100
Level = 0
---------
Name = li4
Ad = 100
Level = 0
---------
继承
Golang 通过匿名字段实现继承的效果:
// 父类
type Human struct {
name string
sex string
}
func (h *Human) Eat() {
fmt.Println("Human.Eat()...")
}
func (h *Human) Walk() {
fmt.Println("Human.Walk()...")
}
// 子类
type SuperMan struct {
Human // SuperMan类继承了Human类的方法
level int
}
// 重定义父类的方法Eat()
func (s *SuperMan) Eat() {
fmt.Println("SuperMan.Eat()...")
}
// 子类的新方法
func (s *SuperMan) Fly() {
fmt.Println("SuperMan.Fly()...")
}
func main() {
// 定义一个子类对象
// s := SuperMan{Human{"li4", "female"}, 88}
var s SuperMan
s.name = "li4"
s.sex = "male"
s.level = 88
s.Walk() // 父类的方法
s.Eat() // 子类的方法
s.Fly() // 子类的方法
}
Human.Walk()...
SuperMan.Eat()...
SuperMan.Fly()...
多态
Go 中接口相关文章:理解Duck Typing(鸭子模型)
Golang 中多态的基本要素:
- 有一个父类(有接口)
// 本质是一个指针
type AnimalIF interface {
Sleep()
GetColor() string // 获取动物的颜色
GetType() string // 获取动物的种类
}
- 有子类(实现了父类的全部接口)
// 具体的类
type Cat struct {
color string // 猫的颜色
}
func (c *Cat) Sleep() {
fmt.Println("Cat is Sleep")
}
func (c *Cat) GetColor() string {
return c.color
}
func (c *Cat) GetType() string {
return "Cat"
}
- 父类类型的变量(指针)指向(引用)子类的具体数据变量
// 接口的数据类型,父类指针
var animal AnimalIF
animal = &Cat{"Green"}
animal.Sleep() // 调用的就是Cat的Sleep()方法, 多态
不同接收者实现接口
type Mover interface {
move()
}
type dog struct {
name string
}
值类型接收者实现接口:可以同时接收 值类型 和 指针类型。
Go 语言中有对指针类型变量求值的语法糖,dog 指针dog2
内部会自动求值*dog2
// 可以同时接收 值类型 和 指针类型
func (d dog) move() {
fmt.Println(d.name, "is moving")
}
func main() {
var m Mover
var dog1 = dog{"dog1"}
m = dog1 // 可以接收值类型
m.move()
var dog2 = &dog{"dog2"}
m = dog2 // 可以接收指针类型
m.move()
}
指针类型接收者实现接口:只能接收指针类型。
// 只能接收指针类型
func (d *dog) move() {
fmt.Println(d.name, "is moving")
}
func main() {
var m Mover
// 无法接收指针类型
// var dog1 = dog{"dog1"}
// m = dog1
//m.move()
var dog2 = &dog{"dog2"}
m = dog2
m.move()
}
一道面试题:以下代码能否通过编译?
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = "你是个大帅比"
} else {
talk = "您好"
}
return
}
func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
不能。修改 var peo People = Student{}
为 var peo People = &Student{}
即可。
通用万能类型
interface{}
表示空接口,可以用它引用任意类型的数据类型。
// interface{}是万能数据类型
func myFunc(arg interface{}) {
fmt.Println(arg)
}
type Book struct {
auth string
}
func main() {
book := Book{"Golang"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
}
Golang 给 interface{}
提供类型断言机制,用来区分此时引用的类型:
注意断言这个操作会有两个返回值
func myFunc(arg interface{}) {
// 类型断言
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value = ", value)
fmt.Printf("value type is %T\n", value)
}
}
一个接口的值(简称接口值)是由一个 具体类型 和 具体类型的值 两部分组成的。
这两部分分别称为接口的动态类型和动态值。
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
switch 判断多个断言:
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}