快速入门 go 语言笔记,参考了各种大佬的 blog 。
一、下载安装
国内站点 :https://golang.google.cn
IDE : Goland
(2020.12月以后破解有些困难 见 zhile.io, 无法试用的话可以搜一个注册码,失效的也可以,验证成功后断网进入软件,再使用 zhile.io)
自行配置 go sdk ,go root 、go path新建项目, 创建 main.go
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
注:
右键新建文件会遇到如下错误 :
https://blog.csdn.net/my_miuye/article/details/103706162
执行命令 :go run XXX.go
编译命令 : go build XXX.go
形成可执行文件 exe
二、基本语法
1、go 风格 :
一个问题只有一个解决方案
例如 : 逐行编译(末尾自动加分号)、 严格格式化 gofmt -w XXX.go
(帮助格式化代码)……
建议每行不要超过 80 个字符。
2、文档:
https://golang.google.cn/pkg/
3、 数据类型 :
(以下开始和 java 产生严重分歧)
1)基本数据类型 : 数值型 、 非数值型
数值型 : 整数数值型 (int、int8、int16…uint、uint8…byte)、 浮点数值型(float32 、 float64)
非数值型 : 字符型 (用 byte 保存单个字母)、布尔(bool)、字符串(string !!区别于java)
2)复杂数据类型 : (派生数据类型)
指针 (Pointer)
数组
结构体(struct)
管道 (Channel)
函数
切片(slice)
接口(interface)
map
注 : 如何查看一个变量的数据类型 ?
var unknow = 123
// 查看类型 %T
fmt.Printf("类型 : %T", unknow)
fmt.Println()
// 查看字节 %d
fmt.Printf("字节 :%d",unsafe.Sizeof(unknow))
go 在保证程序正常运行的前提下,应使用尽可能小空间变量类型
3、浮点数
跟 java 一样,分为单精度、双精度, 支持 e 表示科学计数法 , 也会损失精度。
因为 , 存储方式是将小数拆分成 “符号位 + 指数位 + 尾数位” (即科学计数法)存储在计算机中
golang 小数默认类型 float64, 开发中推荐使用 float64
(与 java 相同, java 默认 double)
4、字符如何保存?
golang 没有 char 为字符单独设计的类型,所以, 字符用 byte 保存。
golang 的字符串用固定长度的字符连接起来, 即用固定长度的 byte 序列表示 string。
golang 的字符串也是不可变数据类型。
var str string = "这是一句话 , 天生 utf-8 ,没有乱码 \n"
fmt.Println(str)
// 对于特殊格式字符串 , 反引号输出带格式 string
var content string = `这里可以带任何格式 :
换行!`
fmt.Println(content)
注 : 字符串拼接 + 放在前一行末尾
4、基本数据类型的默认值
数值型均为 0 , bool 默认 false , 字符串 ""
5、基本数据类型之间的转换
必须强制类型转换, 不像 java 一样可以自动转型!!(低精度到高精度也不可以)
语法 :
//数据类型强制转换
var convert float64 = float64(unknow)
fmt.Printf("强制转换 %T -> %T ", unknow, convert)
注 : 变量 + 数字, 此时数字按默认值 即 int64 跟随OS位数 。 二次赋值可能报错。
6、基本数据类型 与 string 转换
fmt.Sprintf("", XX)
go 中只能通过 strconv 包中的函数进行转换!
var t string = strconv.FormatFloat(float64(price),'f',1,64)// 方法2
fmt.Printf("%T : %s \n", t, t)
// string 转基本数据类型
var string2Float, _ = strconv.ParseFloat("12.23445", 64)
fmt.Printf("%T : %f \n", string2Float, string2Float)
注:
Parse 返回两个值,下划线表示忽略其中一个返回值。
bit 位对应接收变量的 bit 位 。
其他类型同理, 需要查看官方文档。
string 转基本数据类型,如果不能转换, 会直接转为该基本类型的默认值。
7、指针
指针变量已经指向了一个内存空间,但存放的是后面变量的地址。
指针变量同样有自己的地址
var pointer *int8 = &i // 变量i 的内存地址
fmt.Println(pointer)
如何取出指针指向的变量的值? 利用 *
8、值类型 与 引用数据类型
(值类型 “一般” 是在栈内存直接分配空间 , 引用类型 通常 在堆也会 gc, 与 java 相同, 但 string 是个例外)
值类型: 基本数据类型 、 数组、结构体
引用数据类型: 指针、切片、管道等
代码
package main
import (
"fmt"
"strconv"
"unsafe"
)
func main() {
// hello world
fmt.Println("hello world")
// 有符号整数
var i int8 = 100
fmt.Println("i : ", i)
// 无符号整数
var j uint16 = 256
fmt.Println("j : ", j)
// 程序员需要根据 数据范围 对变量进行限制
// 注意 1 : byte ~ uint8
// 注意 2 : int 不限定位数,默认与系统位数相同 (多为 64 位)
var k byte = 255 // 256 会 overflow
fmt.Println("k : ", k)
var unknow = 123
// 查看类型 %T
fmt.Printf("类型 : %T \n", unknow)
// 查看字节 %d
fmt.Printf("字节 :%d \n",unsafe.Sizeof(unknow))
// 浮点型
var price float32 = 12.67
fmt.Println("price : ", price)
// 字符与字符串
var char1 byte = 'a'
fmt.Println(char1)
var char2 byte = '3'
fmt.Println(char2)
fmt.Printf("char1 = %c , char2 = %c \n", char1, char2)
// 溢出情况 : 用范围更大的数值变量,如int
var char3 int = '李'// 字符常量用单引号表示
fmt.Printf("%c", char3)
var str string = "这是一句话 , 天生 utf-8 ,没有乱码!"
fmt.Println(str)
// 对于特殊格式字符串 , 反引号输出带格式 string
var content string = `这里可以带任何格式 :
换行!`
fmt.Println(content)
//数据类型强制转换
var convert float64 = float64(unknow)
fmt.Printf("强制转换 %T -> %T \n", unknow, convert)
// 基本数据类型转 string
var float2String string = fmt.Sprintf("%f", price) // 方法1
fmt.Printf("%T \n", float2String)
var t string = strconv.FormatFloat(float64(price),'f',1,64)// 方法2
fmt.Printf("%T : %s \n", t, t)
// string 转基本数据类型
var string2Float, _ = strconv.ParseFloat("12.23445", 64)
fmt.Printf("%T : %f \n", string2Float, string2Float)
// 指针
var pointer *int8 = &i // 变量i 的内存地址
fmt.Println(pointer)
fmt.Println(*pointer)// 取出指针变量指向的地址的值
fmt.Println()
}
三 、算术运算符
基本与 java 相同
1、 自增,自减不能参与赋值运算。 只能作为独立运算
2、取模 本质 a % b = a - ( a / b ) * b
3、自增,自减 只能写在变量后面 , a++
4、运算符的优先级
单目运算、赋值运算符 从右到左 , 如 !、 sizeof ;
其余都从左到右!
注 :
,
也是运算符, 优先级最低;
&
返回变量的地址, 放在变量前 &a
给出实际地址;
*
表示指针变量
5、不支持 三目运算符 , 与 go 的单一设计理念相违背
6、:=
是声明并赋值,并且系统自动推断类型,不需要var关键字
四、 获取键盘输入
fmt.Scanln() 行输入
与 java 不同,是可以直接通过地址赋值给变量
// 键盘输入
var name string
fmt.Println("请输入姓名:")
fmt.Scanln(&name)
fmt.Println("姓名:", name)
也可以通过占位符 配合 Scanf 作为格式化输入
占位符号 参考 :
https://www.cnblogs.com/junneyang/p/6069248.html
五、流程控制
1、 分支控制
常规 if else , 但是 golang 支持在 if 条件中定义一个变量,并且只在条件语句作用域中起作用。
if age := 20; age > 18{
fmt.Println("你的年龄是 :",age)
}
注 :
即使只有一条语句也不能省略括号。
也有 switch , 注意不需要 break !case 后面只要是一个表达式, 只要有返回值即可。
switch key{
case XX, XX, XX... :
....
....
...
default
.....
}
switch 也可以不带表达式, 当 if else 使用
注意:
fallthrough 关键字 , 表示 switch 穿透 , 走到这个关键字跳入下一个 case 条件。
type switch ,等学过 interface 后补充 ,x interface x.(type)
。
2、循环控制
第一种 ,for 常规三条件使用。第二种是单一条件, 第三种无条件即死循环。
最后两种使用方式并不习惯 , 结合 for 内部使用 if 实现代替 while。
s := "abcdefg"
for i := 0; i < len(s); i++ {
fmt.Printf("%c", s[i])
}
注: 此方式遍历 按照字节逐个遍历,汉字 utf - 8 对应 3 个字节,会出现乱码!需要使用 []rune
转换 string。 newS = []rune(s)
for-range 遍历不会出现问题,默认按照字符的方式遍历 。 但是 index 会出现不连续!
// for range
s = "我是谁"
for index, val := range s {
fmt.Printf("index=%d , val=%c \n", index, val)
}
output :
index=0 , val=我
index=3 , val=是
index=6 , val=谁
注 : TMD !golang 中没有 while 和 do while 。
3、 break 与 continue
golang 可以指定跳出位置,配合 label 使用。
// break 默认跳出最近循环, golang 通过 label 指定跳出位置
labelName :
for i := 0; i < 5; i++{
for j := 0; j < 10; j++{
fmt.Printf("i = %d :: j = %d \n", i, j)
if (i == 3 && j == 7) {
//break
break labelName
}
}
}
注 : continue 同理也可以配合标签
4、goto
golang 支持 goto , 还是不推荐使用,造成流程混乱。
一般 配合 if 和 label 使用 。
六、函数 和 包
1、golang 中函数和方法是有区别的
2、函数与变量的作用域(是否包外可见)是通过名称首字母大小写来确定的,大写即包外可见
3、值传递与引用传递 : 与 java 类似基本数据类型都是值传递, 即复制一份值到栈空间, 然后改变栈空间的值。但是!!! go 中的数组是引用传递,与 java 不同!因为 go中有指针,如果希望函数内修改函数外部值,可以传递指针变量。不需要像 java 一样包成引用数据类型。
4、为了符合 go 的设计初衷, 一种事物有一种解。go 是不支持函数重载的。(go 有可变参数,所以可以达到调用相同函数,传递不同参数)
5、go 中函数也是一种数据类型,可以赋值给一个变量
6、函数本身可以作为形参,传递给其他函数 func myfun(param func(int,int) int, p1 int, p2 int) {...}
7、自定义数据类型关键字 type , 像对原有的数据类型起别名(相当于,但不是等同于同一个数据类型), type myTypeName string
可以创建 myTypeName 类型的 string 变量,声明在调用之前。
.......
// 函数
sv := sum
fmt.Println(sv(10,5))
.......
func sum(a int, b int) int {
return a + b
}
8、很方便区别, go 支持对返回值命名
func sumAndSub(a int, b int) (sum int, sub int) {
sum = a + b
sub = a - b
return //对返回值赋值后,不需要返回sum sub
}
// 调用
.....
p1 := 13
p2 := 11
res1, res2 := sumAndSub(p1, p2)
fmt.Printf("变量1 : %d, 变量2 : %d , sum : %v, sub : %v" , p1, p2, res1, res2)
......
注 : 下划线 占位符可以忽略返回值, day1 提到过。
9、可变参数 参数名后加 ...
func testFunc(p1 int, args... int) int {....}
表示 1 到多个参数
注 :
args 是 slice 切片, 理解为可变数组。
可变参数需要放在形参列表最后。
代码
package main
import "fmt"
func main() {
// ++ --
var a int8 = 56
// var b int8 = a-- // golang 禁止
a--
var b int8 = a - 1
fmt.Printf(" b : %d \n", b)
// 键盘输入
var name string
fmt.Println("请输入姓名:")
fmt.Scanln(&name)
fmt.Println("姓名:", name)
// 流程控制
if age := 20; age > 18{
fmt.Println("你的年龄是 :",age)
}
for i:= 0; i < 10; i++ {
fmt.Printf("第 %d 次循环 \n", i )
}
s := "abcdefg"
for i := 0; i < len(s); i++ {
fmt.Printf("%c", s[i]) // 此方式遍历 按照字节逐个遍历,汉字 utf - 8 对应 3 个字节会出现乱码
}
fmt.Println()
// for range
s = "我是谁"
for index, val := range s {
fmt.Printf("index=%d , val=%c \n", index, val)
}
// break 默认跳出最近循环, golang 通过 label 指定跳出位置
labelName :
for i := 0; i < 5; i++{
for j := 0; j < 10; j++{
fmt.Printf("i = %d :: j = %d \n", i, j)
if (i == 3 && j == 7) {
//break
break labelName
}
}
}
// 函数
sv := sum
fmt.Println(sv(10,5))
p1 := 13
p2 := 11
res1, res2 := sumAndSub(p1, p2)
fmt.Printf("变量1 : %d, 变量2 : %d , sum : %v, sub : %v" , p1, p2, res1, res2)
}
func sum(a int, b int) int {
return a + b
}
func sumAndSub(a int, b int) (sum int, sub int) {
sum = a + b
sub = a - b
return //对返回值赋值后,不需要返回sum sub
}
init 函数 和 go 文件执行顺序
1、全局变量初始化最先执行
2、init 函数 : 在 main 之前执行, 一般用于完成对只声明未初始化的变量的初始化工作。init 函数在引包的时候就已经被执行了了,即 被引入文件的 init 先于 引入文件的 init 函数。
3、main 函数 : 程序主入口
匿名函数
p1 := 100
p2 := 20
// 匿名函数
myFun1 := func(a int, b int) int{
return a * b
} (p1, p2)// 调用位置
fmt.Println(myFun1)
myFun2 := func(a int, b int) int{
return a / b
}
fmt.Println(myFun2(p1, p2))
注 : myFun1 、 myFun2 是函数类型变量
全局匿名函数
var (
// 全局匿名函数
Sum = func (a int,b int) int {
return a + b
}
)
注 : 匿名函数结尾括号 相当于调用这个函数的传参
。。。
// 2.结果管道
resultChan := make(chan *Result, 128)
// 3.创建工作池
createPool(64, jobChan, resultChan)
// 4.开个打印的协程
go func(resultChan chan *Result) {
// 遍历结果管道打印
for result := range resultChan {
fmt.Printf("job id:%v randnum:%v result:%d\n", result.job.Id,
result.job.RandNum, result.sum)
}
}(resultChan)
。。。
闭包
闭包不太好理解,函数式编程中,如果一个函数引用了外部变量,那么他们构成了一个整体。如果,操作闭包对象的方法改变闭包对象关联属性,相当于操作了同一个对象。
// 累加器
func AddUpper() func(int) int{
// 下面的代码相当于构成了 java 中的一个 class
var base = 5
return func(x int) int {
base += x // 因为函数引用了函数外的变量 , 跟 base 一起形成了闭包
return base
}
}
// 调用
// 闭包
add := AddUpper() // 函数调用,此时 add 为累加器的返回函数 (如果不带形参列表,则函数主题作为变量,)
fmt.Println(add(1))// 6
fmt.Println(add(1))// 7 由于闭包所以构成了一个整体,相当于对于同一个对象的方法进行二次调用
fmt.Println(add(1))// 8 闭包的概念相当于 java 中的 class ,构成了一个实例对象, 此时的 add 相当于对于累加方法的三次调用
fmt.Println()
add2 := AddUpper()// 相当于 java 中 new 了一个新对象
fmt.Println(add2(1))// 6
闭包的好处 : 保存以前使用过的变量
defer
遇到defer 语句先不执行 , defer 存在于单独的 defer 栈中 。
函数执行完毕后将 defer 语句出栈。
用于函数的收尾工作, 例如函数结束时需要释放连接。
func testDefer(p1 int, p2 int) int {
defer fmt.Println("defer1 p1: ", p1)// step 3
defer fmt.Println("defer2 p2: ", p2)// step 2
sum := p1 + p2
fmt.Println("sum :", sum) // step 1
return sum
}
....
// 调用
fmt.Println("call test defer : ",testDefer(14, 27)) // step 4
....
注 :
如果 defer 语句涉及到 变量值的话, 入 defer 栈的时也将“变量当时的值”拷贝一份入栈。
最终 defer 语句出栈时,还原的是defer 语句入栈时的变量值。
变量作用域补充
1、只要在函数外面定义的变量就是全局变量, 作用域整个包有效
2、如果全局变量首字母大写, 那么该变量整个程序有效 (其他包通过包名调用)
常用内置函数 (转)
常用内置函数
close: 用于发送方关闭chan,仅适用于双向或发送通道。
len、cap: 用于获取数组、Slice、map、string、chan类型数据的长度或数量,len返回长度、cap返回容量;
new、make: new用于值类型、用户定义类型的内存分配,new(T)将分配T类型零值返回其指向T类型的指针;make用于引用类型(Slice、map、chan)内存分配返回初始化的值,不同类型使用有所区别。
make(chan int) 创建无缓冲区的通道
make(chan int,10) 创建缓冲区为10的通道
make([]int,1,5) 创建长度为1,容量为5的slice
make([]int,5) 创建容量长度都为5的slice
make(map[int] int) 创建不指定容量的map
make(map[int] int,2) 创建容量为2的map
copy、apped: 用于复制slice与为slice追加元素;
print、println: 用于打印输出;
panic、recover: 用于错误处理;
delete: 用于删除map中的指定key
代码
package main
import "fmt"
var (
// 全局匿名函数
Sum = func (a int,b int) int {
return a + b
}
)
func main() {
p1 := 100
p2 := 20
// 匿名函数
myFun1 := func(a int, b int) int{
return a * b
} (p1, p2)// 调用位置
fmt.Println("乘 : " , myFun1)
myFun2 := func(a int, b int) int{
return a / b
}
fmt.Println("除 :" , myFun2(p1, p2))
// 调用全局匿名函数
fmt.Println("加 :" , Sum(p1, p2))
// 闭包
add := AddUpper() // 函数调用,此时 add 为累加器的返回函数 (如果不带形参列表,则函数主题作为变量,)
fmt.Println(add(1))// 6
fmt.Println(add(1))// 7 由于闭包所以构成了一个整体,相当于对于同一个对象的方法进行二次调用
fmt.Println(add(1))// 8 闭包的概念相当于 java 中的 class ,构成了一个实例对象, 此时的 add 相当于对于累加方法的三次调用
fmt.Println()
add2 := AddUpper()// 相当于 java 中 new 了一个新对象
fmt.Println(add2(1))// 6
// defer 存在于单独的 defer 栈中 , 函数执行完毕后将 defer 语句出栈, 用于函数的收尾工作
fmt.Println("call test defer : ",testDefer(14, 27)) // step 4
}
// 累加器
func AddUpper() func(int) int{
// 下面的代码相当于构成了 java 中的一个 class
var base = 5
return func(x int) int {
base += x // 因为函数引用了函数外的变量 , 跟 base 一起形成了闭包
return base
}
}
func testDefer(p1 int, p2 int) int {
defer fmt.Println("defer1 p1: ", p1)// step 3
defer fmt.Println("defer2 p2: ", p2)// step 2
sum := p1 + p2
fmt.Println("sum :", sum) // step 1
return sum
}
七、 错误
golang 中错误处理比较繁琐,没有 try catch 和 throw 。 经常需要逐层判断 err 是否为 nil 特别啰嗦,并且需要显示传递 err 。
一般成熟的工程对 err 都有统一的封装方式、代码风格和 recover 机制,这里不赘述 。
panic :异常, 程序直接会挂掉,需代码 recover。
error :错误, 不会影响程序的运行。release 程序需要有将 panic 转成 err 的机制。
区别 java 名称, java 异常 exception 是可以 try catch 的;错误 error 一般都不可恢复 (死锁、爆栈)。
注意 : golang 中的 error 一般放在返回值的最后一个参数。
八、容器
数组
这数组的定义可真奇怪。
var name [len] type
名称 长度 类型
注 :形参是 parameter,实参是 argument
go 中, 数组在参数传递中是 形参传递, 函数运行时会复制数组的值, 值传递。
(区别 java!)
func main() {
// 随机生成三个数 ,放入数组, 并反转打印
var arr [3] int
rand.Seed(time.Now().UnixNano()) // 当前纳秒值作为随机数种子
for i := 0; i < len(arr); i++ {
arr[i] = rand.Intn(50) // n 为 [ 0, n) 随机范围
}
fmt.Println(arr)
// 换位
for i := 0; i < len(arr)>>1; i++ {
swap(&arr, i, len(arr) - 1 - i)
}
fmt.Println(arr)
}
func swap(arr *[3]int, a int, b int) {
t := (*arr)[a] // 指针取用数组实际值
(*arr)[a] = (*arr)[b]
(*arr)[b] = t
}
注意 :
[]int 与 [n]int 是不同类型,不限制数组长度则为切片类型!
切片 slice
切片是数组的一个引用, 遵循引用传递。使用方式与数组一致, 但是长度是可变的,可以简单理解为一个动态数组。
1、数组得到切片
myslice := arr2[1:4]
fmt.Println("元素 = ", myslice)
fmt.Println("容量 = ", cap(myslice))
注意 : 算头不算尾 。 arr[:]
从头切到尾。
2、切片的本质
一个结构体(stuct), 包含三个部分 : 指向被引用数组第一个元素地址, 切片长度, 容量。指向被切位置的同一个地址,而不是拷贝一份。slice 本身还有一个地址,指向上面的结构体。
注: 切片的容量 cap
切片的容量是可变一般是元素个数的两倍
3、make 创建切片
var myslice2 []int = make([]int, 4, 5)
myslice2[1] = 123
//myslice2[4] = 46 // 越界! cap 有啥用呢 ?
fmt.Println(myslice2)
注 : 程序员不可见,但 make 底层会创建一个数组, 由 slice 维护 创建引用。
4、创建时赋值
var myslice3 []int = []int{6,7,8}
fmt.Println(myslice3)
5、 切片还可以再次切片!
6、扩容函数 append
var myslice2 []int = make([]int, 4, 5)
myslice2[1] = 123
//myslice2[4] = 46 // 越界! cap 有啥用呢 ?
myslice2 = append(myslice2, 46) //超越长度 [0 123 0 0 46]
myslice2 = append(myslice2, 47) //超越长度 [0 123 0 0 46]
fmt.Println(cap(myslice2))// 容量;10 追加超越长度后,容量自动扩容 2 倍
myslice2[4] = 43 // 扩容后切片维护的数组长度发生变化,可以进行正常赋值
fmt.Println(myslice2) // [0 123 0 0 43 47]
注: 切片也可以追加切片。
数组都是底层维护,程序员不可见。
7、 切片的拷贝问题copy
// 切片的拷贝问题
var myslice4 = make([]int, 2, 2)
copy(myslice4, myslice)
fmt.Println(myslice4) // 由多到少的拷贝会被截断
8、string 与切片
string 底层是 byte[] 也可以进行 切片操作。
map 集合
1、简介
kv 数据结构 , 通常 key 是 string 或 int。
var mapName map[k-type]v-type
注 : map 声明是不会分配内存的,需要 make 分配内存! (数组声明完分配内存)
var myMap map[string]string
myMap = make(map[string]string, 5)
myMap["姓名"] = "alex"
myMap["年龄"] = "26"
fmt.Println(myMap)
注意 : golang 中的 key 是完全无序的!
2、 重置 map
golang 没有清空 map 的接口 , 用 myMap = make(map[string]string)// 给一个空 map,原来的内存变为垃圾被 gc
清空 map。
判断值是否存在 :
// 判断值是否存在
val,ok := myMap["姓名"]
fmt.Println(ok , val)
3、遍历
只能用 for-range
//遍历
for key,val := range myMap {
fmt.Println(key, "::", val)
}
4、map 切片
指的是 slice 的数据类型是 map 。
5、 排序 map
将 key 放入切片 的, 对切片排序 (利用 sort 包
) 。
代码
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// 随机生成三个数 ,放入数组, 并反转打印
var arr [3] int
var arr2 = [...]int{1,2,3,4,5} // ... 代替数组的长度
// for range 遍历
for _,value := range arr2{
fmt.Println(value)
}
rand.Seed(time.Now().UnixNano()) // 当前纳秒值作为随机数种子
for i := 0; i < len(arr); i++ {
arr[i] = rand.Intn(50) // n 为 [ 0, n) 随机范围
}
fmt.Println(arr)
// 换位 与 遍历
for i := 0; i < len(arr)>>1; i++ {
swap(&arr, i, len(arr) - 1 - i)
}
fmt.Println(arr)
// 切片 : 动态数组
// 定义与初始化
myslice := arr2[1:4]
fmt.Println("元素 = ", myslice)
fmt.Println("容量 = ", cap(myslice))
var myslice2 []int = make([]int, 4, 5)
myslice2[1] = 123
//myslice2[4] = 46 // 越界! cap 有啥用呢 ?
myslice2 = append(myslice2, 46) //超越长度 [0 123 0 0 46]
myslice2 = append(myslice2, 47) //超越长度 [0 123 0 0 46]
fmt.Println(cap(myslice2))// 容量;10 追加超越长度后,容量自动扩容 2 倍
myslice2[4] = 43 // 扩容后切片维护的数组长度发生变化,可以进行正常赋值
fmt.Println(myslice2) // [0 123 0 0 43 47]
var myslice3 []int = []int{6,7,8}
fmt.Println(myslice3)
// 切片的拷贝问题
var myslice4 = make([]int, 2, 2)
copy(myslice4, myslice)
fmt.Println(myslice4) // 由多到少的拷贝会被截断
// map 集合
var myMap map[string]string
myMap = make(map[string]string, 5)
myMap["姓名"] = "alex"
myMap["年龄"] = "26"
fmt.Println(myMap)
// curd
delete(myMap, "年龄")
fmt.Println(myMap)
// myMap = make(map[string]string)// 给一个空 map,原来的内存变为垃圾被 gc
// 判断值是否存在
val,ok := myMap["姓名"]
fmt.Println(ok , val)
myMap["年龄"] = "26"
myMap["身高"] = "180"
//遍历
for key,val := range myMap {
fmt.Println(key, "::", val)
}
}
func swap(arr *[3]int, a int, b int) {
t := (*arr)[a] // 指针取用数组实际值
(*arr)[a] = (*arr)[b]
(*arr)[b] = t
}
go 中对面向对象编程 与 传统面向对象编程有很大区别, 因为没有类的概念, 需要使用结构体代替。
注 : 面向对象是一种思想, go 实现了面向对象功能。
九、 结构体 struct
1、 定义
type name struct {
fieldName type
....
}
注 : 结构体在 go 中是值传递! name 直接指向内存空间。如果field 中有 slice 或 map 需要先 make 再使用结构体。
type Mystruct struct {
name string
mySlice []float64
ptr *int
myMap map[string]string
} // 未分配空间的引用类型是 nil , 无法赋值
...
var v1 MyStruct
v1.mySlice = make([]float64, 5)
v1.mySlice[0] = 5.5
2、 结构体是值类型,这里与 java 不同!
如果 struct1 := struct2 , 那么stuct1 和 struct2 不指向同一地址。(其实是一个copy操作) 如果想像 java 一样对对象进行引用传递, 需要借助指针。
var p *Person = &Person{XX,"XX"} // 创建时为结构体赋初始值
(*p).XXX = "XX" // 标注使用方式,等效于直接 点属性
3、结构体属性的地址是连续的
4、tag
给结构体的字段 加一个 tag , 序列化 json 的时候,给字段别名。
type Mystruct struct {
Name string `json:"name"`
}
十、 方法
自定义类型都有方法,方法与类型绑定。 (type 创建的自定义类型,不只局限于结构体)
1、方法与函数的区别
函数没有绑定数据类型, 方法需要指定绑定的数据类型 。
type Person struct {
Name string
}
func (p Person) hello() {
fmt.Printf("my name is %s", p.Name)
}
func main() {
var man *Person = &Person{"Alex"}
man.hello()
}
注意 :
go 中自定义类型很奇怪, 可以对基本类型进行自定义。
谁调用方法 ,调用者被当作实参传递给方法。
2、String() 方法实现
如果一个类型,实现了 String() 方法, fmt.Println 会调用 String() ,类比 java 的
注 : 结构体创建方式要改变
// 重写了 Person 的 String
var man *Person = &Person{"Alex"}
man.hello()
fmt.Println(man)
fmt.Println(&man) // man 是指针变量赋初始值, & 还显示地址
var woman Person
woman.Name = "fei"
fmt.Println(woman)
fmt.Println(&woman) // woman 的 & 显示的事 String() ????
十一、面向对象
嵌套结构体(继承)
继承的目的是为了归类和基本的代码复用, 虽然说 java 的继承在维护复杂度较高的工程时,让人头大,但是有总比没有好。
golang 通过组合实现继承,其实就是子类的属性包含父类的引用或者父类 struct。(这里的父类和子类就是被继承者和继承者)
1 、不管首字母大小写, 嵌套结构体可以使用继承的全部方法和属性
type blackMan struct {
Person // 继承
skill string
}
2、就近原则,先找自身方法和属性,(实现继承的 overwrite)
3、多继承,(多嵌套几个匿名结构体)如果发生重名问题,调用需要指定匿名结构体名称
接口 interface (多态)
golang 的核心就是面向接口编程。 我理解 golang 的设计思想,一种方式只有一种结果,本身是与多态相违背的。如果想要使用多态特性要依赖接口实现。空接口 可以表示任意类型,go 中可以传空接口。但是空接口 interface 的传递会引发一些列调试的问题, 以及难以预料的 panic,这些都需要在代码编写的时候做考虑 。
注 :
golang 的空接口不存值。 golang 中空接口的传递,其实发生了类型转换。多态即编译时不能确定接口类型,运行时决定调用哪个实现方法。
类型断言 :
v := i.(T) // 会panic
// 这种方式不会引发 panic
v,ok := i.(T)
“鸭子模式” :
“一个东西看来长得像鸭子, 叫声像鸭子, 走路像鸭子,我们就认为它是鸭子。”
golang 的接口实现不像 java 一样需要显式实现, 认为一个struct 实现了 interface 的所有方法就证明实现了这个接口。因为这种松散耦合会让 struct 多实现接口,导致代码阅读不流畅。 需要 coder 对代码进行有效的目录组织
网友评论