适合有其它编程语言经验(最好是C)的同学,快速上手go语言的相关特性,了解go语言的运行细节,并结合许多实用的go伪代码来服务于真实场景。
GO语言简介
诞生原因
- 目前编程语言不能合理的利用多核CPU
- 软件系统复杂度越来越高,缺乏简洁高效。主要问题:
1.风格不统一
2.对硬件的调度,运行速度不够
3.处理大并发不够好
- C/C++运行速度够快,但是编译速度慢,存在内存泄漏等问题需要解决。
特点
静态编译语言的安全和性能+动态语言开发维护的高效率
- 从C中继承表达式语法,控制结构,基础数据结构,调用参数传值,保留C一样的编译执行方式及弱化的指针。
- 引入包机制,每个.go必须属于一个包
- 垃圾回收,内存自动回收
天然支持高并发
(1)语法层面支持并发,实现简单
(2)goroutine,轻量级线程,可实现大并发处理,高效利用多核
(3)线程间管道通信机制
(4)函数返回多个值
(5)CPS(communicating sequential process)并发模型
(6)切片slice,延时执行defer
语法规范
- 保持语法和代码风格高度规范
go命令
- gofmt main.go # 直接shell输出,自动修正缩进代码
参数:-w 输出重定向至文件
语法规范
- 2 + 4 * 5 //运算符两侧加空格
- 大括号唯一写法
func main(){
} // 正确
func main()
{
} // 错误
- 每行超过80字符,使用","换行
- 编译时,每行自动添加;
- 没有用的包等语法默认必须强制删除,否则无法通过编译。
标识符命名规则
(1)英文大小写,_,0-9
(2)不能数字开头
(3)严格区分大小写
(4)不包含空格
(5)_,空标识符,仅作为占位符,本身的值会被忽略
(6)不能系统关键字
- 驼峰法: stuName xxxYyyZzz
导出名(共有私有)
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出**(共有)**的。例如,Pizza
就是个已导出名,Pi
也同样,它导出自 math
包。若首字母小写(私有)只能本包中使用。
pizza
和 pi
并未以大写字母开头,所以它们是未导出的。
在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。
执行代码,观察错误输出。
然后将 math.pi
改名为 math.Pi
再试着执行一次。
package main
import (
"fmt"
"math"
) //注意是()
func main() {
fmt.Println(math.Pi)
}
数据类型与变量
数据类型
整型类型
- 默认的int,uint长度与运行的操作系统有关。
- 转义符 T% 显示类型,unsafe.Sizeof()显示长度
- go中的 nil = null
浮点数类型
- 存放方式,浮点数=符号+指数+尾数
- 浮点数都是有符号的
- 浮点数长度固定,不受操作系统影响
- .512, 5.12, 5.1234e2
字符串类型
- go中没有专门的字符类型,储存单个字符(英文byte,中文rune)。
- go的字符串由一个字节为一个单位,对比C是由一个字符为单位。(英文一个字符一个字节,中文一个字符两个字节)
- 字符串赋值后无法任何改变。
- 双引号识别转义字符,反引号以原生形式输出(输出源代码、防止攻击)。
- “+”拼接字符串(长字符串换行“+”留在上一行,原因自动;)。
基本数据类型转换
不同数据类型任何情况不能自动转换(低精度到高精度等),必须使用强制转换(显式转换)。
- 强制转换语法
var int32_i int32 = 100
var int64_i int64 = int64(i) //变量i转换为int
1.转换后int32_i还是int32,只是把转换后的值赋予int64_i。
2.高精度->低精度(int64->byte),会溢出处理。
-
int32 + 10 为int32不能赋值到int64
var n1 int64 = 100 var n2 int32 = 100 n1 = n2 + 10 //n2 + 10为int32不能直接赋值int64,编译不通过 n1 = int64(n2) + 10 //编译通过
-
var n1 int32 = 12 var n2 int8 = 0 n2 = (int8)n1 + 128 //编译不通过,128不是int8 n2 = (int8)n1 + 127 //编译通过,但是溢出处理
基本数据类型与string转换
- 基本->string
(1)fmt.Sprintf
str = fmt.Sprintf("%d" , num_int)
(2)strconv
#strconv.Itoa int快速转为str
#FormatInt
strconv.FormatInt(int64(original_int64_varialble), radix)
#第一个输入必须int64,第二个为转换的进制2-36
#FormatFloat
#FormatBool
- string->基本, strconv
#strconv.ParseBool
b , _ = strconv.ParseBool(str)
#多返回值:bool与err,不需要的_忽略接收
#strconv.ParseInt
默认返回值数据类型为int64,需要其他int类型需要强制转换。
- 逻辑转换错误
#若string为“hello",强制转换成int,则转换失败默认赋值为默认值0。
值类型和引用类型
- 值类型:基本数据类型int,float、bool、string、arrauy、struct。
#变量直接储存值,内存通常分配在栈。(编译器逃逸分析可能分配在堆)
- 引用类型:指针、slice切片、map、管道chan、接口interface。
#变量储存地址,这个地址对应的空间才是存储的值,内存通常在堆上分配,当没有变量引用这个地址时,变成垃圾被GC回收。
变量
零值
没有明确初始值的变量声明会被赋予它们的 零值。
零值是:
数值(整型、浮点型)类型为 0
,
布尔类型为 false
,
字符串为 ""
(空字符串)。
多变量声明
1.var n1, n2, n3 int
2.var n, name, n3 = 1 , “tom”, 2
3.n, name, n3 := 1 , “tom”, 2 //类型推导
短变量声明
func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
函数外的每个语句都必须以关键字开始(var
, func
等等),因此 :=
结构不能在函数外使用
//全局变量
var{
n1 = 1
name = "tom"
n2 = 3
}
作用域
- 函数内部声明的变量为局部变量,作用域仅限于函数内部
- 函数外部声明的变量为全局变量在整个包都有效,如果首字母大写,可以在整个程序import后跨包使用
- 如果在if/for代码块中声明,仅在该代码块有效
运算符
- go语言不支持三元运算符
算术运算符
除法
- 运算结果保留,根据运算的数据类型中高精度的决定:
10 / 4 = 2 //运算的两个数都是整型,因此转换为整形,数据溢出,结果为2
var i float = 10 / 4 //还是2
var i float = 10.0 / 4 //2.5,需要浮点数参与运算
取模
- a % b = a - a / b * b //a / b需要向下取整到整型
自增、自减
a := b++ //错误,只能单独b++使用
if i++ > 0 { //错误
-
强制语法统一,独立使用i++,减少歧义
-
无–i,++i
关系运算符
- 只返回bool值
逻辑运算符
- 返回bool值
短路与、短路或
- && 也叫短路与,如果第一个为false,则第二个条件的代码不会运行判断,最终结果为false。
- || 也叫短路或,如果第一个条件为true,则第二个条件不会判断,最终结果为true。
赋值运算符
-
从右向左运算
-
=, +=, -=, *=, /=, %=
-
<<=, >>=, &=, ^=, |=,
<<= // c = c << 2 , 右移两位后赋值c
&= // c = c & 2 , 与2位与后赋值c
指针
-
**区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。**移动指针需要使用unsafe包
-
变量名不等于数组开始指针,如果p2array为指向数组的指针, *p2array不等于p2array[0]。
-
数组做参数时, 需要被检查长度。
结构
返回值
多值返回
1.只接受多值返回中的部分值
// 使用_进行占位
a, _, c = fun_return_abc()
2.两种返回方式
//
func swap(x, y string) (string, string) {
return y, x
}
//
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
if
语法细节
- 就算逻辑块内只有一条语句也必须有大括号。
if a > 0 {
fmt.Println("a")
}
- if判断条件建议不带()
- else,else if必须不换行
if ... {
...
} else if{
...
} else {
...
}
if 语句定义的变量作用域在if内
同 for
一样, if
语句可以在条件表达式前执行一个简单的语句。
- 该语句声明的变量作用域仅在
if
逻辑块之内。 - 同时在if语句判断条件中只能写判断,不能赋值等操作。
执行细节
- 多分支控制只有一个出口,即多个if条件满足,对第一个为真的执行,执行完跳出条件判断。
if 条件1 { // 1为真
...
} else if 条件2 { // 2为真
...
} else if 条件3 {
...
} else {
...
}
//执行完1后跳出条件判断
package main
// 推荐的引入包的方式
import (
"fmt"
"math"
)
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else { //必须else不换行,保证这种格式
fmt.Printf("%g >= %g\n", v, lim)
}
// 这里开始就不能使用 v 了
return lim
}
func main() {
// 由内层开始执行,先pow最后println
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
for
执行流程
1.循环变量初始化 i++
2.执行循环条件 i <= 10
3.判断条件(返回bool值);若为真,执行循环操作(语句块1)
4.执行循环变量迭代
5.反复1234,知道循环条件False
for i := 1 ; i <= 10 ;i++ {
语句块1
}
for 包含 while
while/do-while
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
for {
if 循环条件表达式{ //决定dowhile还是while
break
}
循环操作
循环变量迭代
}
死循环
如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
- 搭配break设置跳出条件
使用细节
- 循环调节返回值一定为bool
- 可以只跟判断条件
for i < 10 {
i++
}
- 数组、字符串遍历
- 下标
for i := 0; i < len(str); i++ {
fmt.Println(str[i])
}
- 注意下标遍历字符串:go中全部按照utf8编码,字符串格式按照一个字节为单位划分,中文为三个字符串,因此需要str转rune再遍历。
str = []rune(str)
- for range方式
- 默认转换为rune,按照字符遍历,不需要转换
for index, val := range str {
fmt.Println(val)
}
switch
语法细节
- 无break
switch 表达式 {
case 表达式1, 表达式2, ...:
语句块1
case 表达式3, 表达式4, ...:
语句块2
default:
语句块
}
-
switch、case后可以跟常量、变量、函数的返回值
-
case后的数据类型必须和switch后的数据类型严格统一、完全一致
-
case后可以跟多个表达式
case 5, 10, n1 :
语句块
-
若case后为常量(只要是变量就可以),则不同的case后的常量之间不能数值重复
-
default语句块不是必备的
高级用法
- 无条件的switch,使多重判断更简洁(类似if else的逻辑)
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
//在switch内声明变量,不推荐
func main() {
switch t := time.Now() {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
- fallthrough 穿透特性
如果在case语句后增加fallthrough,则会继续判断下一个case
case a:
语句块1
fallthrough //会判断下面的case
case b:
语句块2
- type switch
判断interface变量中实际指向的变量类型
break、continue、goto、return
break
- 中断for,跳出switch
使用label控制跳出层数
label1: {
label2: {
label3: {
break label1 // 若没有标签默认跳出一层,也就是跳出label3这一层,跳出最近的一层for
// label1 等于break break break
}
}
}
- 标签名字随意,格式固定。
continue
- 结束本次循环,继续执行下一次循环;也可以使用label来确定是跳过那一层循环
goto
- 无条件的转移到程序指定的行,通常与条件语句配合使用,实现条件转移,跳出循环等功能。
- 一般主张不使用goto
return
- 跳出所在的方法或函数。
包
- 一个包=一个文件夹,本质创建不同的文件夹来存放不同的程序文件
- go的每一个文件都属于一个包,通过包的形式来管理文件和项目目录结构
- package、import
包的作用
- 区分同名函数、变量等标识符
- 当程序文件多时,构建合理的项目结构
- 作用域,控制函数、变量等访问范围
包名与引入
-
通常与当前.go上一级文件目录名称保持一致,一般为小写字母
-
首字母大写(可以是变量名、函数名)为可导出(公有、可导出、跨包使用),小写(私有、不可导出)
-
注意区分包名和包所在上级目录文件夹名称
//被调用文件,在/.../dir下
package utils //1.utils为包名,package规定打包的包名 2.package第一行
//调用文件
package use_utils
import (
"/.../dir" //在dir路径下寻找包
)
utils.TestFun() //用包名调用函数
- import默认路径从环境变量$GOPATH下的src开始,编译器自动从src寻找引入
- 包名过长可以使用别名(原包名不可继续使用)
import (
utils "/.../dir" //命名包别名为utils,后续引用**只能**使用别名,不能用原包名
)
-
同一包下不能有相同的函数名(以包做命名空间)
-
一个包也就是一个文件夹下可以有多个.go文件,但是都属于一个包,因此不能有相同的函数名和全局变量名
-
如果需要编译成一个可执行文件,则需要将这个包声明为main包,其它的库文件名称可以自定义
go build -o \bin\test.exe(保存路径) \...\main(需要编译的项目目录)
- 引入的包必须使用,不使用如下:
import (
"a"
_ "b" //引入不使用
)
默认项目结构
- src存放源码
- package存放,编译后的库文件(类似.dll,.so)
函数
func 函数名 (形参列表) (返回值列表) {
执行语句
return 返回值列表
}
-
函数中的变量是局部的,函数外不生效
-
基本数据类型和数组都是值传递,即值拷贝。在函数内修改,不会影响到原来的值。(不同的物理地址,可能变量名相同)
-
引用类型传递,传递的是被引用值的地址或者索引,因此修改的是同一个物理地址(指针),但是地址本身还是值传递
-
严格不允许函数名称重名(不支持函数重载)
-
在go中函数也是一种数据类型,可以赋值给一个变量,则该变量是一个函数类型变量,通过此变量可以调用函数
func myFun(funvar func(int, int) int, num1 int, num2 int) {
//funvar是函数变量形参名字
return funvar(num1, num2)
}
- 为了简化数据类型定义,go支持自定义数据类型(与typedef很类似,别名);一般在程序import后就type,在函数中局部生效
type mySum func(int, int)int //mySum等价于func(int, int)int函数数据类型
//注意在虽然本质都是同一种类型,但是编译器认为两个是两种数据类型
- 函数返回值命名
//主要作用是返回时按照函数返回值列表顺序接收
func test(n1 int, n2 int) (n3 int, n4 int){
n3 = n1
n4 = n2
return //直接return,按照
}
n3, n4 = test(1,2)
- 使用"_"作为占位符,忽略返回值
- 支持可变参数(动态参数个数);如果有可参数需要将可变参数放在形参列表最后
//支持0到多个参数
func sum(arg... int) sum int { //arg为标识符,可以是其它名称
}
//支持1到多个参数
func sum(n1 int, arg... int) sum int {
}
//求一个到多个int的和
func sum(n1 int, vars_int... int) int {
sum := n1
for i :=0; i < len(vars_int); i++ {
sum += var[i]
}
return sum
}
内存分区与形参实参
-
栈区(通常存放基本数据类型)、堆区(通常存放引用数据类型)、代码区(存放代码)、全局区、常量区…(go编译器存在逃逸分析机制)
-
栈区函数结束释放,堆区当这个引用类型没有任何变量引用这个地址被GC回收
-
运行一次函数在栈区中拥有一个命名空间(下面的demo适用于栈区的基本基本数据类型)
func test (n1 int) { //使用栈区中test栈区中的n1来接收
n1 = n1 + 1
fmt.Println(n1) //输出11
}
func main {
n1 := 10 //在栈区中的main栈区创建n1
test(n1) //main栈区n1对test栈区n1赋值,实参对形参赋值
fmt.Println(n1) //调用完毕,回收test栈区,输出10
}
栈区 ------ test栈区
|
--- main栈区
|
--- ...
递归调用
- 每一次调用都会为test生成一个新的栈,最后逐层回收
- 局部变量相互独立,只是名字相同
func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=" , n)
}
//输出 2 2 3
func test(n int) {
if n > 2 {
n--
test(n)
}else{
fmt.Println("n=" , n)
}
}
//输出 2
init函数
-
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被go运行框架调用。
-
文件有全局变量定义:变量定义->init->main
-
init主要作用:完成初始化工作
-
引入的包有init时完整执行顺序:
package main //1
import ( //2
"/.../utils"
)
func init() { //6
}
func main() { //7
}
package utils //3
func init() { //4
}
func main() { //5
}
匿名函数
- 如果我们某个函数只希望使用一次,可以考虑匿名函数,匿名函数也可以多次调用
使用方式
- 定义匿名函数时直接调用(只能调用一次,使用较多)
func main() {
res1 := func (n1 int, n2 int) int {
return n1 + n2
}(10, 20)
}
- 将匿名函数赋值给一个变量,再通过变量调用匿名函数(可以多次使用,使用频率低,意义:可以在函数内定义函数)
func main() {
func_name := func (n1 int, n2 int) int {
return n1 + n2
}
res1 := func_name*(10, 20)
}
全局匿名函数
- 将函数赋值给全局变量,则可以在整个程序中通过全局变量调用函数
闭包
- 闭包就是一个函数和与其有关的引用环境组合的一个整体(本质是一个函数)
- 闭包=函数返回函数+函数内被使用的变量
// 累加器
func AddUpper() func (int) int {
// 闭包开始
var n int = 10
return func (x int) int { 返回的是一个匿名函数,但是函数引用了函数外的n,因此匿名函数和n形成了一个整体,构成了闭包
n = n + x
return n
}
// 闭包结束
}
func main() {
f := AddUpper()
fmt.Println(f(1)) // 10 + 1 = 11
fmt.Println(f(2)) // 11 + 2 = 13
fmt.Println(f(3)) // 13 + 3 = 16
}
// 返回的数据类型是fun(int)int
// 关键是返回的函数使用到了那些变量,函数和它使用的变量构成闭包
demo分析
- 闭包的优势:向函数传递一次参数,参数可以被持久化记录,不用每次调用都反复传入参数;当函数的部分参数需要短暂持久化(但是需要有接口可以修改),使用闭包
// 需求:1.判断文件名是否有.后缀,若有返回直接文件名(直接返回name.xxx),若无加上.xxx返回(name.xxx)
2..xxx可以修改即可
3.被调用的时候,大部分时间后缀使用.jpg
func MakeSuffix (suffix string) func (string) string {
return func (name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
} else {
return name
}
}
}
f := MakeSuffix(".jpg")
f("1") //返回1.jpg
f("2.jpg") //返回2.jpg
//闭包 = 匿名函数 + suffix
defer
- 需要创建资源(数据库连接、文件句柄、锁等),但是需要在函数执行完毕后及时释放资源 ,因此提供defer(延时机制)
一个值defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
package main
import "fmt"
func main() {
//当执行到defer语句时,会语句将压入defer栈(独立栈)
//**当函数执行完毕后,按照先入后出的方式出栈执行**
defer fmt.Println("!") //3
defer fmt.Println("world") //2
fmt.Println("hello,") //1
}
func main() {
n1 := 1
n2 := 2
// 在defer语句入栈时,会将相关的**值拷贝**(和原变量为两个独立的变量)同时入栈
defer fmt.Println(n1) //1
defer fmt.Println(n2) //2
n1++
n2++
fmt.Println(n1+n2) //5
}
func test() {
file = openfile()
defer file.close() //不用考虑具体啥时候关闭文件句柄
...
}
多个值defer
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
函数参数的传递方式
值类型默认使用值传递,引用类型默认使用引用传递
如果希望函数外修改函数内变量,只能引用传递,利用&以指针的方式操作变量
- 值传递
地址1 地址2
| -> |
值1 值2
拷贝效率低
值类型:基本数据类型int、float、bool、string,数组,结构体
- 引用传递
地址1 -> 地址2
| |
值1 值1
拷贝效率高
引用类型:指针、切片、map、管道chan、interface等
常用系统函数
buildin,内建函数不用import直接使用
字符串函数
-
len(str),按字节统计字符串长度或数组长度(go使用utf8,ascii字符(字母和数字)占一个字节,一个汉字(或者其它语言)占用三个字节)
-
中文字符串遍历,r := []rune(str)
-
字符串转整形,n, err := strconv.Atoi(""),可以利用err判断输入是否是纯数字;整数转字符串,str = strconv.Itoa(),不存在不能转换
-
字符串转byte,转为ascii,var bytes = []byte(“str”);byte转字符串(ascii),str = string([]byte{97, 98, 99})
-
10进制转2、8、16进制,str = strconv.FormatInt(123, 2)
-
查找子串是否在指定字符串,strings.Contains(“abcddd”, “abc”) // true
-
统计字符串由几个指定的子串,strings.Count(“abcddd”, “ddd”) // 3
-
不区分大小写比对字符串,strings.EqualFold(“str”, “str”);不区分大小写,str == str
-
返回子串第一次出现的index,若无返回-1,strings.Index(“adcddd”, “ddd”) // 3;返回子串最后一次出现的index,若无返回-1,strings.LastIndex(“adcddd”, “ddd”)
-
使用子串替换另一个子串,n为第n个,n=-1表示全部替换,strings.Replace(“abcddd”, “ddd”, “eee”, n),可以是变量传入
-
按照指定字符拆分字符串,strings.Split(“1,2,3,4”, “,”),返回一个字符串数组
-
大小写转换,全部变为大写或小写,strings.ToLower(str),strings.ToUpper(str)
-
去掉字符串两边的所有空格,strings.TrimSpace(" test ")
-
将字符串左右两边指定的字符去掉,不考虑去除去字符串的顺序,strings.Trim("!ahelloa!", “!”) // hello
-
将字符串左(右)侧指定字符去掉,不考虑去除去字符串的顺序,strings.TrimLeft("!hello!", “!”)、strings.TrimRight("!hello!", “!”)
-
是否以指定的字符串开头、结尾,strings.HasPrefix(“abc”, “a”)、strings.HasSuffix(“abc”, “c”)
日期和时间函数
内置函数(buildin)
异常处理
-
go追求简洁优雅,不支持try…catch.finally
-
go引入的异常处理方式:defer、panic、recover
-
go抛出一个panic异常 -> 在defer中的recover中捕获这个异常 -> 处理异常
func test() {
defer func () {
err := recover() // 捕获异常,将异常返回err变量(error类型)保存
... // 处理异常
}() // 匿名函数,无输入,无输出
... // 此处抛出错误
... // 跳过错误,抛出异常后继续执行
}
func test() {
defer func () {
if err := recover() ; err != nil { // 捕获 + 判断
...
} // 捕获异常,将异常返回err变量(error类型)保存
... // 处理异常
}() // 匿名函数,无输入,无输出
... // 此处抛出错误
... // 跳过错误,抛出异常后继续执行
}
自定义错误
- errors.New(“错误说明”)创建自定义错误,返回error类型的值
- panic,接受一个interface{}类型的值(可以是任何 值)作为参数,接收error,输出error信息,退出程序
// 读取配置文件,目录下有无指定文件名文件
func readConf(name string) (err error) {
if name == "config.ini" {
return nil
} else {
return errors.New("file name error!")
}
}
func test() {
err := readConf("...")
if err != nil {
panic(err)
}
...
}
数组和切片
数组
-
在go中数组时值类型,arrayName为变量名(a),数组为变量数据(1)。(var a int = 1)
-
创建数组后自动为每个数组元素赋零值(详情见零值章节)
-
arrayName地址为数组首地址(=第一个元素的地址),arrayName是值类型,直接存放数组(数组看做一个和int等一样的值);数组地址为:首地址(变量arrayName地址、第一个元素地址)+ n * 数组类型长度(例如:int64,为8个字节,地址加8)
-
数组时多个相同数据类型的数据的组合,数组一旦声明,其长度就固定,不能动态变化。(数组长度也是数组信息的一部分)
-
注意切片和数组的区别,var arr []int 这种写法arr是切片
-
数组可以是基本数据类型也可以是引用类型,但是只能全部元素是一种数据类型
-
数组下标从0开始
-
go数组属于值类型,在默认情况下进行值传递,因此进行值拷贝不会影响原数组
-
想函数外修改函数,需要传递指针,使用引用传递
初始化数组的方式
- 定义数组:var arrayName [n]float64;访问、赋值数组元素;arrayName [0] = …
- 使用顺序:开辟空间->赋值->访问
var array [3]int = [3]int{1, 2, 3}
var array = [3]int{1, 2, 3}
var array = [...]int{1, 2, 3} // ...系统自动判断大小
var array = [...]int{1: 2, 0: 1, 2: 3} // 数组下标+数据
array := [...]int{1, 2, 3} // 类型推导,最简洁
遍历
- 常规遍历
for i := 0; i < len(array); i++ {
...
}
- for-range遍历
for index, value := range array {
...
}
// 1. 第一个返回值index是数组下标
// 2. 第二个value是在该下标位置的值
// 3. index、value都是在for内部可见的局部变量
// 4. 若遍历数组时不需要index或value,使用_替代
// 5. index和value是两个变量接受返回值,名称不固定,习惯使用index和value
二维数组
var arr [n1][n2]int //n1,n2为数组真实大小,不是从0开始
arr[n3][n4] = i //n3,n4为索引从0开始计数
&arr[0][0] //第一个一维数组首地址
&arr[1][0] //第二个一维数组首地址
var arr [n][n]int = [...][n]int{
{...},{...}} // 只有一维数组也就是第一个位置可以...,因为是按照初始化时的个数来自动确定个数
var arr = [n][n]int{
{...},{...}}
- 底层寻址方式和一维数组一致,数组元素变为数组指针;也就是二维数组保存着n个一维数组的首地址,然后通过一维数组的首地址访问一维数学组。
遍历
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ { // 注意len(arr[i])
...
}
}
for i1, v1 := range arr {
for i2, v2 := range v1 { // 注意range v1
}
}
切片
-
切片是数组的引用(修改slice,原数组也会改变),切片的长度可以变化,是一个动态的数组。(底层追加的方式是1.有容量的概念存在一定的冗余;若冗余不足,2.拷贝原切片指向的数组的值到一个新的空间,然后创建切片返回新的切片,然后通过切片变量接收)
-
数组a[1](有确定大小),切片a[](无确定大小)
-
与数组相比,遍历、访问元素(silce[n])、len()用法相同
-
定义:var sliceName []int
-
切片有容量的概念(容量可以动态变化),也就是目前能存放最大元素的个数,使用cap()函数可以查看;一般容量是元素个数的两倍
-
切片本质是一个结构体:1.首个元素的首地址 2.长度 3.容量\
-
切片必须定义后,赋值后再使用
-
切片可以引用切片(或其它引用变量)
-
在初始化时不能越界,但是可以动态增长。可以使用append函数动态追加元素
-
append、copy的所有对象只能是切片,不能是数组
slice3 = append(silce3, 400, 500, 600) // append的方式是创建一个新的切片(数组),然后返回切片通过赋值传递给切片
slice3 = append(slice3, slice1...) // 切片追加切片
使用方式
- 定义一个切片,通过引用的方式去引用已经创建好的数组;数组已经创建,再创建切片使切片引用数组
- 简写:arr[0:end],arr[:end];arr[start:len(arr)], arr[:end];arr[0:len(arr)],arr[:]
var array [3]int = [3]int{1, 2, 3}
silceName := array[0:2] // 引用array数组起始下标为0,终止下标为2**(不包含2)**
- 通过make函数(还可以创建映射和通道)创建切片,此时切片还是结构体,指向一个数组空间(但是数组匿名无法通过数组名访问),只能通过切片访问。数据结构模型不变:切片(结构体)->数组空间(数组匿名或数组名可访问)
- make创建切片的同时创建一个数组(空间),数组底层由切片维护,数组无法通过数组的正常方式访问
var slice_name []type = make([]type, len, cap(可以忽略))
var slice []int = make([]int, 4, 8)
slice[0] = 100
- 直接定义
var silce_name []int = []int{1, 2, 3}
遍历
- for循环常规遍历
for i := 0; i < len(slice); i++ {
...
}
- for-range
for i, v := range slice {
... //i为index,v为value
}
拷贝
-
拷贝和被拷贝的两个数据空间是独立的
-
copy(slice1, slice2) // slice1内容拷贝到slice2,若容量1>2优先数组下标0开始,剩下的不拷贝;2>1从数组下标0开始拷贝,2多余剩下的内容不变
string和slice
- string也是一个结构体:1.数组首地址、2.长度,底层指向一个byte数组;因此string可以使用切片操作,slice也指向byte数组
// 数组截取
str := "hello,world!"
str_slice := str[7:] // world!
- 使用slice修改字符串,
// 错误
str[0] = 'z' // str本质是字符串结构体,无法通过数组的方式直接寻址,修改字符串
// 将string->[]byte/[]rune(byte或rune切片),然后修改
arr1 := []byte(str)
/*
1.如果处理中文需要转为[]rune
2.原因:go中string为byte数组(以字节为单位),go使用utf-8,ascii内的字符(英文、数字等)一个字节,其他语言(中文等)三个字节,因此要使用rune
*/
arr1[0] = 'z'
map(映射)
- key-value
- var mapName map[keytype]valuetype;var name map[int]string
- 声明后(未分配内存,数组会直接分配内存,赋零值),再make(分配内存),再使用,可以动态调整大小
//
var mapName map[int]string
mapName = make(map[int]string, 10)
//
var mapName = make(map[int]string, 10)
// 底层自动make
var mapName map[int]string = map[int]string{1:"no1", 2:"no2", } // 注意,
mapName := map[int]string{
1:"no1",
2:"no2", // 注意,
}
- key唯一,如果出现重复的key赋值,会覆盖同名的key-value,同时无序
- key-value在map中存放是无序的(与key大小、添加顺序无关),可能每次输出的顺序都不一样
- len(map),一个map有len(map)对键值对
- map是引用类型的数据,传递时函数外可修改
- map自动扩容(动态增长),不会报错;切片必须手动append扩容,否则panic错误
- map的value是结构体最好,可以管理复杂的数据
key
- key的数据类型可以是:bool、数字、string、channel,还可以是以上几个类型的接口、结构体、数组
- 不能是:slice、map、function,因为无法用==判断
- 通常是:int、string
value
- 通常为数字、string、map、struct
- 嵌套map,value为map
// 一个学生信息包含:name、sex、address
//为最外层map分配空间
studentMap := make(map[int]map[string]string) // 不输入大小默认为1
// 为具体的学生分配空间
studentMap[1] = make(map[string]string, 3)
studentMap[1]["name"] = "..."
studentMap[1]["sex"] = "..."
studentMap[1]["address"] = "..."
增删查改
- 增、改
map["key"] = value // 如果key还没有就增加,如果已经存在就更新(也就是修改)
- 删除
delete(map, "key") // 如果key不存在,不会报错,也不会操作
// 如果全部删除所有的key,推荐2
// 1. 遍历key
// 2. map=make(),make一个新的同名map,让原来的成为垃圾被gc回收
- 查找
val, res := map[“1”] // val储存返回键值,res返回是否存在键值,true存在,false不存在
遍历
- 只能使用for-range遍历
for k, v := range map {
...
}
切片
- 数组元素是map的动态数组
var mapSlice []map[int]string // 定义map切片
mapSlice = make([]map[int]string, 2) // 为slice分配内存
mapSlice[0] = make(map[int]string, 2) // 为map元素分配内存
// 在slice中追加map
newMap := map[int]string {
...
}
mapSlice = append(mapSlice, newMap)
排序
-
key排序,key无序,每次输出可能顺序都不一样
-
排序思路,先将map的key放入切片,对切片排序,遍历切片,按照key输出map
var keys []int
for k, _ := range map {
keys = append(keys, k) //遍历插入key切片
}
sort.Ints(keys) // 使用自带方法排序
for _, k := range keys {
map[keys] //按照key的顺序遍历输出
}
转载:https://blog.csdn.net/viafcccy/article/details/117073708