Golang项目的组织管理
1、说明
go1.5之前都是用GOPATH来管理项目的,安装好go环境之后,需要配置GOROOT和GOPATH环境变量,项目代码要写在GOPATH指定的目录中。
在Go1.14及之后的版本中启用了Go Module模式
之后,不一定非要将代码写到GOPATH目录下,所以也就不需要我们再自己配置GOPATH了,使用默认的即可。
所以,项目开发中直接使用go module模式即可。
2、VSCode配置Go项目
首先要配置GOPROXY
,在vscode中下载go插件,当写go代码之后,vscode提示要下载一些支持go环境的工具,配置GOPROXY之后才可以成功。
这些下载的东西会走默认设置的GOPATH指向的路径。为了防止vscode将这些数据下载到c盘,可以设置这个环境变量的位置。
在控制终端输入下面命令即可配置
go env -w GOPROXY=https://goproxy.cn,direct
//默认是GOPROXY=https://proxy.golang.org,direct,但是国内访问不到
*** 通过go module模式
创建一个模块(项目)
1、首先创建一个空目录GoProjects,然后在这个目录中再创建一个空目录project1
,这个作为一个模块。在控制台进入project1目录,然后输入命令go mod init project1
初始化这个模块目录,会出现go.mod
这个配置文件,内容如图显示。
2、然后在project1这个模块目录下面创建一个main.go
文件,然后执行。
![在这里插入图片描述](https://img-blog.csdnimg.cn/23bfbb1088ca46598f32c11c88470aa4.png
3、在同一个模块(项目)中,导入包。
4、导入不同模块中的包。首先创建一个新的模块project2
,同样的操作,先初始化这个模块,即 go mod init project2
,在这个模块中创建两个包,然后让project1的程序调用这个新的模块中的包。因为project1要加载另一个模块的包,所以要先在project1模块的go.mod中配置
。
Golang中的函数、包和错误处理
1、函数
*** 基本语法
package main
import (
"fmt"
)
// 参数列表:不能有var关键字。此外,go的函数没有默认参数,也就是不能在参数列表中给参数赋值
/*
返回值列表: 如果没有返回就不写返回值类型,也不用加括号;
如果有一个返回值可以加括号也可以不加;
如果有多个返回值,就必须要加括号
*/
//接受返回值的时候,如果有不需要的返回值可以使用 _ 表示忽略这个返回值。
func cal(n1 float64, n2 float64) (float64, float64) {
fmt.Println(n1, n2)
return n2, n1
}
func main() {
fmt.Println(cal(1.0, 3.0))
}
*** 函数使用的注意事项
- 函数的形参列表可以是多个,返回值列表也可以是多个。
- 形参列表和返回值列表的数据类型
可以是值类型和引用类型
。 - 函数的命名遵循标识符命名规范,首字母不能是数字,
首字母大写该函数可以被本包文件和其它包文件使用
,类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似privat - 函数中的变量是局部的,函数外不生效
基本数据类型和数组默认都是值传递的
,即进行值拷贝。在函数内修改,不会影响到原来的值。- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),
可以传入变量的地址&,函数内以指针的方式操作变量
。从效果上看类似引用。
package main
import (
"fmt"
)
func getSum(v1 int, v2 int) int {
return v1 + v2
}
// 为了简化数据类型定义,Go 支持自定义数据类型,通过关键字type来实现
// 比如 type myInt int; 这样就可以使用myInt来替代int
// 对于函数类型也是一样,一般是在类型比较冗长的情况下简化数据类型定义
// 函数类型在使用type的时候,只需要fanc关键字、参数列表、返回值列表即可,不能写函数名
// 重新定义的是一种函数类型,通过查看函数类型%T就可以理解为什么不加函数名
type funSum func(int, int) int
// 这里的第一个参数就可以直接使用funSum类型
func mySum(fun funSum, v1 int, v2 int) int {
return fun(v1, v2)
}
func main() {
fmt.Println(mySum(getSum, 1, 2)) // 3
}
package main
import (
"fmt"
)
// 计算两数相加和相减
/*
函数支持返回值命名,可以在函数中直接使用返回值名称
如果有返回值名称,可以使用也可以不使用,如果不使用,在return的时候就必须返回结果
*/
func getSumSub(v1 int, v2 int) (sum int, sub int) {
// 直接用返回值变量名来接收返回结果
sum = v1 + v2
sub = v1 - v2
// var sum string = "" 错误,不能重名
return //这里必须要写return
}
func main() {
sum, sub := getSumSub(12, 16)
fmt.Println("sum = ", sum, " sub = ", sub) // 28 -4
}
package main
import (
"fmt"
)
/*
函数支持可变参数,可变参数是一个切片,可以通过索引得到每一个元素
要求可变参数在参数列表的最后,否则会出错,所以只能有一个可变参数
*/
func getSum(n1 float32, args ...int) int {
sum := int(n1)
for i := 0; i < len(args); i++ {
sum += args[i]
}
return sum
}
func main() {
sum := getSum(10, 11, 12, 12)
fmt.Println("sum = ", sum)
}
2、init函数
*** 基本介绍
每一个源文件都可以包含一个 init 函数
,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用
。
*** init函数的注意事项和细节
- 如果一个文件同时包含全局变量定义,init 函数和 main 函数,则执行的流程
全局变量定义->init函数->main 函数
,全局变量即使声明在init函数和main函数后面也是这样执行的。
package main
import (
"fmt"
)
var glo = test()
func test() int {
fmt.Println("test...")
return 90
}
func init() {
fmt.Println("init... ", glo)
}
func main() {
fmt.Println("main...", glo)
}
2. 当导入了其他包,里面的源文件也有init函数和全局变量,他们的执行顺序如下:
3、匿名函数
*** 基本介绍
Go 支持匿名函数,匿名函数就是没有名字的函数
,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
*** 匿名函数的两种使用方式
package main
import (
"fmt"
)
func main() {
// 匿名函数
// 1、在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。
result := func(val int, vals ...int) int {
sum := val
for i := 0; i < len(vals); i++ {
sum += vals[i]
}
return sum
}(10, 20)
fmt.Println(result) // 30
// 2、将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
funvar := func(val int, vals ...int) int {
sum := val
for i := 0; i < len(vals); i++ {
sum += vals[i]
}
return sum
}
result = funvar(1, 2, 3, 4, 5, 6, 7, 8, 9)
fmt.Println(result) // 45
}
*** 全局匿名函数
如果将匿名函数赋给一个全局变量
,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
package main
import (
"fmt"
)
var (
// 将匿名函数赋值给全局变量
funvar = func(val int, vals ...int) int {
sum := val
for i := 0; i < len(vals); i++ {
sum += vals[i]
}
return sum
}
)
func main() {
// 通过全局变量调用匿名函数
result := funvar(1, 2, 3)
fmt.Println(result)
}
4、闭包
*** 基本介绍
闭包就是一个函数和与其相关的引用环境组合
的一个整体(实体)。
代码说明:
- AddUpper 是一个函数,返回的数据类型是 fun (int) int
- 闭包的说明
- 返回的是一个匿名函数, 但是这个
匿名函数引用到函数外的 n ,因此这个匿名函数就和 n 形成一个整体
,构成闭包。 - 可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。
- 当我们反复的调用 f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。
- 我们要搞清楚闭包的关键,就是要
分析出返回的函数它使用(引用)到哪些变量
,因为函数和它引用到的变量共同构成闭包。
*** 闭包的最佳实践
^^ 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
^^ 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
^^ 要求使用闭包的方式完成
^^ strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
package main
import (
"fmt"
"strings"
)
func makeSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
f := makeSuffix(".jpg")
fmt.Println(f("weater"))
fmt.Println(f("spring.png"))
fmt.Println(f("author.jpg"))
}
说明:
- 返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为 返回的函数引用到 suffix 这个变量
- 如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。
5、函数的defer(延迟机制)
*** 基本介绍
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
*** defer的注意事项和细节
- 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中【暂时称该栈为 defer 栈】, 然后继续执行函数下一个语句。
- 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制)
- 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。
*** defer机制的最佳实践
defer最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源
。
说明:
1、在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close() 、defer connect.Close()
2、在 defer 后,可以继续使用创建资源.
3、当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
4、这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
6、函数参数的传递方式
*** 基本介绍
参数传递方式就只有值传递和引用传递
两种方式。
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝
,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低
。
*** 值类型和引用类型
*** 值传递和引用传递使用特点
7、变量的作用域
- 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部。
- 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效。
- 如果变量是在一个代码块,比如 for / if 中,那么这个变量的的作用域就在该代码块。
8、字符串常用函数
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
// 1、len(str) 统计字符串的长度,单位是子节数
// 英文数字一个子节,中文三个字节
var str = "Hello,世界"
fmt.Println(len(str)) // 12个子节
// 2、字符串遍历,如果有中文,不能直接遍历字符串,需要先切片 r := []rune(str)
str2 := "HELLO北京"
r := []rune(str2)
for i := 0; i < len(r); i++ {
fmt.Printf("%c", r[i]) // 格式化输出字符
}
fmt.Println()
// 3、字符串转整数: n, err := strconv.Atoi("12")
n, err := strconv.Atoi("12")
if err != nil {
fmt.Println("转换整数错误 ", err)
} else {
fmt.Println("转换成整数的结果 ", n)
}
// 4、整数转换成字符串: str = strconv.Itoa(n)
str3 := strconv.Itoa(123)
fmt.Printf("%T, %v\n", str3, str3)
// 5、字符串转 []byte: var bytes = []byte("hello")
var bytes = []byte("hello北京")
fmt.Printf("bytes = %v\n", bytes)
// 6、[]byte 转 字符串: str = string([]byte{97, 98, 99})
str4 := string([]byte{97, 98, 99})
fmt.Println(str4)
// 7、10进制 转2、8、16进制,并且返回字符串: str = strconv.FormatInt(123, 2)
str5 := strconv.FormatInt(123, 2)
fmt.Printf("123对应的二进制是 = %v\n", str5)
str5 = strconv.FormatInt(123, 16)
fmt.Printf("123对应的十六进制是 = %v\n", str5)
// 8、查找子串是否在指定的字符串中: strings.Contains("main string", "small string")
b := strings.Contains("HELLO, WORLD", "world")
fmt.Println("是否包含在主串中: ", b) // false
b = strings.Contains("HELLO, WORLD", "HELLO")
fmt.Println("是否包含在主串中: ", b) // true
// 9、统计一个字符串有几个指定的子串 : strings.Count("ceheese", "e") //4
counts := strings.Count("HELLO", "L")
fmt.Printf("num = %v\n", counts) // 2
// 10、不区分大小写的字符串比较(== 是区分字母大小写的 ): fmt.Println(strings.EqualFold("abc","Abc")) // true
b = strings.EqualFold("abc", "ABC") // 不区分大小写比较字符串
fmt.Println("abc == ABC ", b) // true
fmt.Println("abc == ABC", "abc" == "ABC") // false
// 11、返回子串在字符串第一次出现的index值,如果没有返回-1: strings.Index("NLT_abc", "abc") // 4
index := strings.Index("NLP_世界", "界") // 按照子节来取索引
fmt.Println(index) // 7
// 12、返回子串在字符串最后一次出现的 index,如没有返回-1 : strings.LastIndex("go golang", "go")
index = strings.LastIndex("世界你好呀世界", "世界")
fmt.Println("index = ", index) // 15
// 13、将指定的子串替换成 另外一个子串: strings.Replace("go go hello", "go", "go 语言", n) n 可以指定你希望替换几个,如果 n=-1 表示全部替换
str6 := "golang clang java python clang java golang golang"
str7 := strings.Replace(str6, "golang", "2009go", 2) //不会影响到str6字符串
fmt.Println(str6) // golang clang java python clang java golang golang
fmt.Println(str7) // 2009go clang java python clang java 2009go golang
// 14、按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
strArr := strings.Split("Hello,world,kiko,yoyo", ",")
for i := 0; i < len(strArr); i++ {
// str[0] = Hello str[1] = world str[2] = kiko str[3] = yoyo
fmt.Printf("str[%v] = %v\t", i, strArr[i])
}
fmt.Printf("\nstrArr Type = %T\n", strArr) // strArr Type = []string
// 15、将字符串的字母进行大小写的转换: strings.ToLower("Go") // go strings.ToUpper("Go") // GO
str8 := "golang Hello"
str9 := strings.ToLower(str8)
str10 := strings.ToUpper(str8)
fmt.Println(str9) // golang hello
fmt.Println(str10) // GOLANG HELLO
// 16、将字符串左右两边的空格去掉:strings.TrimSpace(" tn a lone gopher ntrn ")
str11 := " tn a lone gopher ntrn "
fmt.Println(str11)
str12 := strings.TrimSpace(str11)
fmt.Println(str12)
// 17、将字符串左右两边指定的字符去掉: strings.Trim("! hello! ", " !h") // ["hello"] //将左右两边 !和 " "去掉
str13 := strings.Trim("! he!llo! ", "h! ") // "h! "表示三个字符,和顺序没有关系
fmt.Println(str13) // e!llo
// 18、将字符串左边指定的字符去掉 : strings.TrimLeft("! hello! ", " !") // ["hello"] //将左边 ! 和 " "去掉
// 19、将字符串右边指定的字符去掉 :strings.TrimRight("! hello! ", " !") // ["hello"] //将右边 ! 和 " "去掉
// 20、判断字符串是否以指定的字符串开头: strings.HasPrefix("ftp://192.168.10.1", "ftp") // true
// 21、判断字符串是否以指定的字符串结束: strings.HasSuffix("NLT_abc.jpg", "abc") //false
}
9、内置函数(常用)
- len:用来求长度,比如 string、array、slice、map、channel
- new:用来分配内存,主要用来分配值类型,比如 int、float32,struct…返回的是指针。
package main
import (
"fmt"
)
func main() {
num1 := 100
// num1的类型 int, num1的值 = 100, num1的地址 = 0xc000016078
fmt.Printf("num1的类型 %T, num1的值 = %v, num1的地址 = %v\n", num1, num1, &num1)
num2 := new(int)
*num2 = 100
// num2的类型 *int, num2的值 = 0xc0000160b0, num2的地址 = 0xc00000a030, num2指针指向的值 = 100
fmt.Printf("num2的类型 %T, num2的值 = %v, num2的地址 = %v, num2指针指向的值 = %v\n", num2, num2, &num2, *num2)
}
- make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。
10、包(配合首章节)
*** 包的引出
- 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go中,去使用 utils.go 文件中的函数,如何实现? --> 包
- 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? --> 包
- go 的每一个文件都是属于一个包的,也就是说
go 是以包的形式来管理文件和项目目录结构的
.
总之,包的本质就是不同的文件夹来管理不同的程序文件。
*** 包的三大作用
- 区分相同名字的函数、变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数、变量等访问范围,即作用域
*** 包的打包和导入
打包基本语法
package 包名
引入包的基本语法
import "包的路径"
*** 包的使用参考前一节的项目组织管理
*** 包使用的注意细节和注意事项
- 在给一个文件打包时,该包对应一个文件夹,比如这里的 user文件夹对应的包名就是user,
文件的包名通常和文件所在的文件夹名一致
,一般为小写字母
。 - 为了让其它包的文件,可以访问到本包的函数,则该
函数名的首字母需要大写
,类似其它语言的public ,这样才能跨包访问。 - 在访问其它包函数,变量时,其语法是
包名.函数名
。 - 如果包名较长,
可以给包名取别名
,如果取了别名,那么就不能再用原来的。 - 在同一包下,不能有相同的函数名(也不能有相同的全局变量名〉,否则报重复定义。
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即 package main .这个就是一个语法规范,如果你是写一个库,包名可以自定义。
*** 导入模块中的包使用步骤
11、错误处理
*** 错误处理引出
说明:
- 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
- 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可
以在捕获到错误后,给管理员一个提示(邮件,短信。。。) - 这里引出我们要将的错误处理机制
*** 基本介绍
- Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理。
- Go 中引入的处理方式为:
defer, panic, recover
- 这几个异常的使用场景可以这么简单描述:
Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
。
package main
import (
"fmt"
"time"
)
func test() {
// 使用defer + recover 来捕获和处理异常
defer func() {
err := recover() // recover是内置函数,可以捕获到异常
if err != nil {
fmt.Println("err = ", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println("result = ", result)
}
func main() {
//测试
test()
for {
fmt.Println("test函数之后的代码...")
time.Sleep(time.Second)
}
}
*** 错误处理的好处
进行错误处理之后,程序可以更加健壮,也可以提供预警代码。
*** 自定义错误
Go 程序中,也支持自定义错误
, 使用 errors.New 和 panic 内置函数
。
- errors.New(“错误说明”) , 会返回一个 error 类型的值,表示一个错误
- panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序。
package main
import (
"errors"
"fmt"
)
// 函数去读取配置文件init.conf的信息
// 如果文件名传入不正确,就返回一个自定义错误
func readConf(name string) (err error) {
if name == "config.ini" {
return nil
} else {
// 返回一个自定义错误
return errors.New("name is not a valid configuration")
}
}
func test() {
defer func() {
err := recover() // recover是内置函数,可以捕获到异常
if err != nil {
fmt.Println("err = ", err)
}
}()
err := readConf("config111.ini")
if err != nil {
//如果读取文件错误,就输出错误信息,并终止程序(如果没有上面的defer处理,就会终止程序)
panic(err)
}
fmt.Println("test继续执行")
}
func main() {
//测试
test()
fmt.Println("MAIN...")
}
Golang中的数组
1、基本介绍
数组就是同一类型的数据的集合,方便数据管理。在Go中,和其他语言不一样,这里的数组类型是值类型
,而不是引用类型。
2、数组定义和内存布局
*** 定义
var 数组名 [数组大小]数据类型
var a [5]int
赋初值 a[0] = 1 a[1] = 30 ...
*** 数组在内存布局
3、数组的使用
*** 数组名[下标索引]
package main
import (
"fmt"
)
func main() {
// 从终端输入5个成绩并且输出
var grades [5]float32
for i := 0; i < 5; i++ {
fmt.Printf("请输入第%d个成绩: ", i+1)
fmt.Scanln(&grades[i])
}
// 输出成绩
for i := 0; i < 5; i++ {
fmt.Println(grades[i])
}
}
几种初始化数组的方式
package main
import (
"fmt"
)
func main() {
// 几种初始化数组的方式
var numArr1 [3]int = [3]int{1, 2, 3}
fmt.Println("numArr1 ", numArr1)
var numArr2 = [3]int{1, 2, 3} // 自动类型推导
fmt.Println("numArr2 ", numArr2)
// 使用[...]固定写法,可以不指定数组的个数,它的长度按照初始化数值的个数确定或者是使用索引赋值的最大索引确定
var numArr3 = [...]int{1, 2, 3, 4} //
fmt.Println("numArr3 ", numArr3, " len(numArr3) ", len(numArr3)) // 4个元素
var numArr4 = [...]int{1: 100, 2: 200, 5: 500} // 将指定的索引位置的数值赋值,未赋值的默认值
fmt.Println("numArr4 ", numArr4, " len(numArr4) ", len(numArr4)) // 6个元素
numArr5 := [...]string{1: "kiko", 2: "yoyo"}
fmt.Println("numArr5 ", numArr5)
numArr6 := [3]string{"nike", "jack"}
fmt.Println("numArr6 ", numArr6)
}
4、数组的遍历方式
*** 方式一
传统方式,for循环和len(arr)配合使用
。
*** 方式二
for-range 结构
遍历
这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素(不仅仅可以访问数组)。
package main
import (
"fmt"
)
func main() {
// for-range
heroes := [...]string{"李白", "韩信", "嬴政"}
for index, value := range heroes {
fmt.Printf("index = %v, value = %v\t", index, value)
fmt.Printf("heroes[%d] = %v\n", index, heroes[index])
}
for _, value := range heroes {
fmt.Printf("%v\t", value)
}
}
5、数组使用的注意事项和细节
-
数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化。
-
数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
-
数组创建后,如果没有赋值,有默认值(零值)。
数值类型数组:默认值为 0
字符串数组: 默认值为 “”
bool 数组: 默认值为 false -
使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值(默认零值) 3 使用数组
-
数组的下标是从 0 开始的
-
数组下标必须在指定范围内使用,否则报 panic:数组越界,比如var arr [5]int 则有效下标为 0-4
-
Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响,比如通过函数参数传递,然后这个函数体内对数组进行了修改,这不会影响函数外的数组实体。
-
如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
-
长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度
Golang中的切片
1、引入
如果需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,此时数组无法满足需求,切片就是这个解决方案。
2、基本介绍
- 切片的英文是 slice
切片是数组的一个引用
,因此切片是引用类型
,在进行传递时,遵守引用传递的机制
。- 切片的使用和数组类似,
遍历切片、访问切片的元素和求切片长度 len(slice)都一样
。 - 切片的长度是可以变化的,因此切片是一个
可以动态变化数组
。 - 切片定义的基本语法:
var 切片名 [ ]类型
比如:var a [ ] int - 切片本质上是对对数组的一个封装,底层引用了一个数组。
3、快速入门
package main
import (
"fmt"
)
func main() {
// 演示切片的基本使用
// 声明初始化一个数组
var intArr [5]int = [...]int{1, 2, 3, 4, 5}
// 声明一个切片
slice := intArr[1:3]
/*
intArr[1:3] 表示slice切片引用到intArr这个数组中的索引1到3这个部分
[1:3] 包左不包右,不能引用数组中不存在的索引
*/
fmt.Println("intArr = ", intArr) // [1 2 3 4 5]
fmt.Println("slice = ", slice) // [2 3]
fmt.Println("slice的元素个数 = ", len(slice)) // [2 3]
fmt.Println("slice的容量 = ", cap(slice)) // 4
fmt.Printf("intArr的地址 = %p\n", &intArr) // 0xc0000126c0
fmt.Printf("slice的地址 = %p\n", &slice) //0xc000008078
fmt.Printf("slice的第一个元素的地址 = %p\n", &slice[0]) //0xc0000126c8
fmt.Printf("intArr索引为1的元素的地址 = %p\n", &intArr[1]) //0xc0000126c8
slice[0] = 999
fmt.Println("slice after change = ", slice) // [999 3]
fmt.Println("intArr = ", intArr) // [1 999 3 4 5]
}
4、切片在内存中的形式
以上面的例子为例
分析:
- slice是一个引用类型
- slice从底层来说,是一个数据结构(struct结构体)
//大致有三个成员组成,一个是指向数组的指针,所以切片是引用类型,它内部是得到了引用数组的指针,通过指针对引用的数组进行操作
//第二个是切片当前的元素的个数
//第三个是容量,表示当前切片能存放的元素的个数,可以继续扩展,有它自己的扩容机制。
type slice struct {
ptr * [2]int
len int
cap int
}
5、切片的使用
方式1
第一种方式是定义一个切片,然后让切片去引用一个已经初始化好了的数组,比如上面的代码。
方式2
通过make
来创建切片
基本语法:var 切片名 [ ]type = make( []type, len, [cap]) // cap可选
参数说明: type: 就是数据类型 ;len : 大小; cap :指定切片容量,可选, 如果你分配了 cap,则要求 cap>=len
.
对上面代码的小结:
- 通过 make 方式创建切片可以指定切片的大小和容量
- 如果没有给切片的各个元素赋值,那么就会使用默认值[int , float=> 0 string =>”” bool =>false]
- 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素。所以切片不管使用何种方式创建,底层都是维护着一个数组。
方式3
定义一个切片,直接就指定具体的数组,使用原理类似make的方式。
方式1和方式2的区别
第一种方式是直接引用数组,这个数组事先就在代码中存在,程序员可见。
第二中方式通过make来创建切片,make底层也会创建一个数组,由切片在底层进行维护,开发者看不见,也无法直接使用数组。
6、切片的遍历
和数组一样两种方式都可以。
7、切片的使用的注意事项和细节讨论
- 切片初始化时 var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。 - 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长.
var slice = arr[0:end] 可以简写 var slice = arr[:end]
var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]
var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]
cap 是一个内置函数,用于统计切片的容量
,即最大可以存放多少个元素。- 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用。
- 切片可以继续切片
- 用
append 内置函数
,可以对切片进行动态追加
上面代码的分析
切片 append 操作的本质就是对数组扩容
go 底层会创建一下新的数组
newArr(安装扩容后大小)
将 slice 原来包含的元素拷贝到新的数组 newArr
slice 重新引用到 newArr
注意 newArr 是在底层来维护的,程序员不可见
append函数会返回一个新的切片,如果要保留原来的切片,可以使用新的切片去接收。上面是用原来的切片接收。从内存图中分析可以知道,既然创建了一个新的数组,那么两个切片自身的修改不会影响到对方
。
-
切片的拷贝操作
切片使用copy 内置函数完成拷贝
对上面代码的说明:
(1) copy(para1, para2) 参数的数据类型是切片
(2) 按照上面的代码来看,拷贝和被拷贝的切片 的数据空间是独立
,相互不影响,也就是说 slice2[0] = “koko”, slice[0]还是等于"foo" -
切片是引用类型,所以在传递时,遵守引用传递机制.
8、string和slice
string 底层是一个 byte 数组
,因此 string 也可以进行切片处理
- string 和切片在内存的形式,以 “abcd” 画出内存示意图
string 是不可变的
,也就说不能通过 str[0] = ‘z’ 方式来修改字符串
- 如果需要修改字符串,可以先将
string --> []byte 或者 []rune --> 修改 --> 重写转成 string
9、二维数组
- 快速入门
- 使用方式
使用方式 1: 先声明/定义,再赋值
语法:var 数组名 [大小][大小]类型
比如: var arr [2][3]int , 再赋值。
使用方式 2: 直接初始化
声明:var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
赋值(有默认值,比如 int 类型的就是 0)
说明:二维数组在声明/定义时也对应有四种写法[和一维数组类似]
var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 [大小][大小]类型 = [...][大小]类型{{初值..},{初值..}}
var 数组名 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 = [...][大小]类型{{初值..},{初值..}}
- 二维数组的遍历
Golang中的map
1、基本介绍
map 是 key-value 数据结构
,又称为字段或者关联数组。类似其它编程语言的集合,在编程中是经常使用到。
2、声明
*** 基本语法
var map变量名 map[keytype]valuetype
说明:
key 可以是什么类型:
golang 中的 map的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组。
通常 key 为 int 、string类型
。
注意: key不可以是slice, map 还有 function 不可以,因为这几个没法用 == 来判断。
value 可以是什么类型:
valuetype 的类型和 key 基本一样。通常为: 数字(整数,浮点数),string,map,struct
。
*** map声明
举例
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
注意:声明是不会分配内存的,初始化需要 make
,分配内存后才能赋值和使用
。
package main
import (
"fmt"
)
func main() {
//声明一个map
var mmap map[int]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
mmap = make(map[int]string, 2)
mmap[0] = "kiko"
mmap[1] = "yoyo"
mmap[2] = "jack"
mmap[1] = "bing"
fmt.Println(mmap)
fmt.Println("-------------------------")
var mp map[string]string
mp = make(map[string]string, 2)
mp["kiko"] = "kiko"
mp["yoyo"] = "yoyo"
mp["jerry"] = "jerry"
mp["jack"] = "jack"
fmt.Println(mp)
}
- map 在使用前一定要 make
- map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
- map 的 value 是可以相同的.
3、初始化使用方式
package main
import (
"fmt"
)
func main() {
// 1、第一种使用方式
// 语法建议将声明和make写在同一行
var mmp1 map[string]string = make(map[string]string, 2)
mmp1["no1"] = "李白"
mmp1["no2"] = "韩信"
mmp1["no3"] = "诸葛亮"
mmp1["no1"] = "孙悟空"
fmt.Println(mmp1)
// 2、第二种使用方式,和第一种方式一样,主要是使用变量的方式不同
cities := make(map[string]string)
cities["n1"] = "北京"
cities["n2"] = "上海"
cities["n3"] = "深圳"
fmt.Println(cities)
// 3、第三种使用方式
languages := map[string]string{
"no1": "Java",
"no2": "Python",
"no3": "JavaScript",
"no4": "Golang", //最后这里也要加上,
}
languages["no3"] = "Js"
fmt.Println(languages)
}
4、map套map练习理解
package main
import (
"fmt"
)
func main() {
// make生成对象, 创建map对象的时候,第二个参数可以不设置
studentMap := make(map[string]map[string]string)
// 内部的map也要make生成
studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "kiko"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "qq.com"
studentMap["stu02"] = make(map[string]string)
studentMap["stu02"]["name"] = "yoyo"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "tube.com"
fmt.Println(studentMap)
fmt.Println(studentMap["stu02"])
fmt.Println(studentMap["stu02"]["address"])
}
5、map 的增删改查操作
6、map的遍历
map元素个数
*** 遍历map套map
package main
import (
"fmt"
)
func main() {
// make生成对象, 创建map对象的时候,第二个参数可以不设置
studentMap := make(map[string]map[string]string)
// 内部的map也要make生成
studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "kiko"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "qq.com"
studentMap["stu02"] = make(map[string]string)
studentMap["stu02"]["name"] = "yoyo"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "tube.com"
for key1, value1 := range studentMap {
fmt.Println(key1)
for key2, value2 := range value1 {
fmt.Println("\t", key2, " = ", value2)
}
}
}
7、map的切片
切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化
了。这里的map个数动态变换表示可以往切片中添加多个map对象,而不是之前直接生成一个map对象,可以往里面不断添加新的元素,自身能够自动扩容,两者不要搞混了。
package main
import (
"fmt"
)
func main() {
// 声明一个map切片
var heroes []map[string]string
// 给map切片创建空间
heroes = make([]map[string]string, 2)
// 增加信息
if heroes[0] == nil {
heroes[0] = make(map[string]string, 2)
heroes[0]["name"] = "韩信"
heroes[0]["age"] = "100"
}
if heroes[1] == nil {
heroes[1] = make(map[string]string, 2)
heroes[1]["name"] = "李白"
heroes[1]["age"] = "120"
heroes[1]["status"] = "active"
}
// 非法,超出了切片的索引范围
// heroes[2] = make(map[string]string)
// if heroes[2] == nil {
// heroes[0] = make(map[string]string, 2)
// heroes[0]["name"] = "诸葛亮"
// heroes[0]["age"] = "100"
// }
// 可以通过append添加新的map对象到切片中
newHero := map[string]string{
"name": "鲁班",
"age": "130",
}
heroes = append(heroes, newHero)
fmt.Println(heroes)
}
8、map排序
*** 基本介绍
- golang 中没有一个专门的方法针对 map 的 key 进行排序。
- golang 中的 map 默认是无序的(新版本是字典序),注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样.。
- golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可。
package main
import (
"fmt"
"sort"
)
func main() {
map1 := make(map[int]int)
map1[10] = 100
map1[1] = 90
map1[3] = 110
map1[7] = 80
map1[5] = 30
fmt.Println(map1)
//如果按照map的key的顺序进行排序输出
//1.先将map的key放入到切片中
//2. 对切片排序
//3. 遍历切片,然后按照key来输出map的值
// 排序方式是使用sort包中的函数,但是它是对切片进行排序,所以要先将key转成切片
var keys []int = make([]int, 1)
for key, _ := range map1 {
keys = append(keys, key)
}
// 排序
sort.Ints(keys) // 升序
fmt.Println(keys)
// 遍历
for _, k := range keys {
fmt.Printf("map1[%v] = %v \n", k, map1[k])
}
}
9、map 使用细节
map 是引用类型,遵守引用类型传递的机制
,在一个函数接收 map,修改后,会直接修改原来的 map。
-
map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
-
map 的 value 也经常使用 struct 类型
,更适合管理复杂的数据(比前面 value 是一个 map 更好)
,比如 value 为 Student 结构体。