【字节跳动青训营】后端笔记整理-1 | Go语言入门指南:基础语法和常用特性解析(一)+ https://developer.aliyun.com/article/1521832?spm=a2c6h.13148508.setting.14.439a4f0ek64lb2
三、复合数据类型
基本数据类型是Go语言世界的原子,它包括整型int,浮点数float32,复数,布尔型bool,字符串string和常量const。
而复合数据类型是以不同的方式组合基本类型而构造出来的。主要有四种:数组、slice、map和结构体。
数组和结构体是聚合类型,它们的值由许多元素或成员字段的值组成。
数组是由同构的元素组成——每个数组元素都是完全相同的类型,结构体则是由异构的元素组成的。
数组和结构体都是有固定内存大小的数据结构,相比之下,slice和map则是动态的数据结构,它们可以根据需要动态增长。
1、数组
package main import "fmt" func main() { var a [5]int //一个可以存放 5 个int元素的数组 a a[4] = 100 fmt.Println("get:", a[2]) fmt.Println("len:", len(a)) b := [5]int{1, 2, 3, 4, 5} fmt.Println(b) var twoD [2][3]int for i := 0; i < 2; i++ { for j := 0; j < 3; j++ { twoD[i][j] = i + j } } fmt.Println("2d: ", twoD) }
数组就是一个具有编号且长度固定的元素序列。对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。不过,在真实业务代码里面很少直接使用数组,因为它长度是固定的,用的更多的是切片。
数组遍历
使用 for range 循环可以获取数组每个索引以及索引上对应的元素:
func showArr() { arr := [...]string{"Go123", "Go456", "Go789"} for index, value := range arr { fmt.Printf("arr[%d]=%s\n", index, value) } for _, value := range arr { fmt.Printf("value=%s\n", value) } }
输出结果:
注意,Go 中的数组是值类型而不是引用类型。当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,不会影响原始数组。
func arrByValue() { arr := [...]string{"Go123", "Go456", "Go789"} copy := arr copy[0] = "Golang" fmt.Println(arr) fmt.Println(copy) }
输出结果:
2、slice
切片不同于数组,可以任意更改长度,也有更丰富的操作。
package main import "fmt" func main() { s := make([]string, 3) s[0] = "a" s[1] = "b" s[2] = "c" fmt.Println("get:", s[2]) // c fmt.Println("len:", len(s)) // 3 s = append(s, "d") s = append(s, "e", "f") fmt.Println(s) // [a b c d e f] c := make([]string, len(s)) copy(c, s) fmt.Println(c) // [a b c d e f] fmt.Println(s[2:5]) // [c d e] fmt.Println(s[:5]) // [a b c d e] fmt.Println(s[2:]) // [c d e f] good := []string{"g", "o", "o", "d"} fmt.Println(good) // [g o o d] }
切片是对数组的一个连续片段的引用,切片是一个引用类型。
切片本身不拥有任何数据,它们只是对现有数组的引用,每个切片值都会将数组作为其底层的数据结构。
slice 的语法和数组很像,只是没有固定长度而已。
创建切片
a.使用 []Type 可以创建一个带有 Type 类型元素的切片
// 声明整型切片 var numList []int //未赋值,numList默认值是nil // 声明一个空切片 var numListEmpty = []int{}
b.使用 make
函数构造一个切片,格式为 make([]Type, size, cap)
package main import ( "fmt" ) func main() { // 创建一个初始长度为 3,容量为 5 的整数切片 slice := make([]int, 3, 5) fmt.Println("切片长度:", len(slice)) fmt.Println("切片容量:", cap(slice)) }
Type
:表示切片的元素类型。size
:表示切片的长度(包含的元素数量)。cap
:表示切片的容量(capability,底层数组的长度,即可以容纳的元素数量上限)。
c.通过对数组进行片段截取创建一个切片
arr := [5]string{"Go123", "Go456", "Go789", "Go1101112", "Go131415"} var s1 = arr[1:4] //左闭右开 fmt.Println(arr) fmt.Println(s1)
slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。
有点类似于Python,但不同于Python,Go不支持负数索引。
切片的长度和容量
一个 slice 由三个部分构成:指针、长度和容量。
指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。
长度对应 slice 中元素的数目;长度不能超过容量。
容量一般是从 slice 的开始位置到底层数据的结尾位置。
简单的讲,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数。
如果切片操作超出上限将导致一个 panic 异常。
s := make([]int, 3, 5) fmt.Println(s[10]) //panic: runtime error: index out of range [10] with length 3
切片元素的修改
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(slice []Type, elems ...Type) []Type 。其中 elems ...Type 在函数定义中表示该函数接受参数 elems 的个数是可变的。这些类型的函数被称为可变函数。
当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。
3、数组与切片的区别
数组(Array)
固定长度: 数组是一种固定长度的数据结构,定义数组时需要指定其长度,并且长度在创建后不能改变。
值类型 : 数组是值类型,当将数组赋值给另一个数组时,会复制数组的内容。
内存 分配: 数组的内存是一次性分配的,所以它们在内存中占据一块连续的存储空间。
声明和初始化: 数组的声明和初始化可以使用大括号 {},也可以在声明时指定元素的值。
// 声明一个包含 5 个整数的数组 var arr [5]int //必须显式指定长度,不能省略 arr := [5]int{1, 2, 3, 4, 5}
切片(Slice)
可变长度: 切片是动态长度的数据结构,可以根据需要进行扩容或缩减。
引用类型: 切片是引用类型,复制切片时只会复制一个引用,而不是整个数据内容。
内存 分配: 切片的底层是由数组支持的,底层数组的长度可能大于切片的长度。
声明和初始化: 切片的声明和初始化使用 make 函数,或者通过从现有数组或切片中切取子集来创建。
// 使用 make 函数创建一个包含 3 个整数的切片 slice := make([]int, 3) // 从现有数组或切片中切取子集创建切片 subSlice := arr[1:3] // 包含索引 1 和 2 的元素 // 直接定义一个切片 不用指定长度,切片会根据元素个数自动确定长度 nums := []int{1, 2, 3, 4, 5}
总结来说,数组和切片都用于存储一组相同类型的数据,但数组具有固定长度和值类型特点,而切片具有可变长度和引用类型特点。通常情况下,切片更加灵活,因为它们支持动态大小调整。
4、map
map 是实际使用过程中最频繁用到的数据结构。在其它语言里叫做字典或者哈希。
package main import "fmt" func main() { m := make(map[string]int) //key的类型是string,value的类型是int m["one"] = 1 m["two"] = 2 fmt.Println(m) // map[one:1 two:2] fmt.Println(len(m)) // 2 fmt.Println(m["one"]) // 1 fmt.Println(m["unknow"]) // 0 r, ok := m["unknow"] fmt.Println(r, ok) // 0 false delete(m, "one") m2 := map[string]int{"one": 1, "two": 2} var m3 = map[string]int{"one": 1, "two": 2} fmt.Println(m2, m3) }
我们可以用 make 来创建一个空 map。这里需要两个类型,第一个是 key 的类型,这里是 string,另一个是 value 的类型,这里是int。我们可以从里面去存储或者取出键值对。可以用 delete 从里面删除键值对。golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。
map是引用类型的,当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
可以在声明的时候直接对map进行初始化:
m := map[int]string{ 1: "Go123", 2: "Go456", 3: "Go789", } fmt.Println(m)
也可以只声明但不初始化,后续通过添加操作将元素添加进map。
map 操作
a.添加元素
// 使用 `map[key] = value` 向 map 添加元素。 m[4] = "Go101112"
b.更新元素
// 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。 m[4] = "GoGoGo"
c.获取元素
// 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。 fmt.Println(m[4])
d.删除元素
//使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete也不会报错。 delete(m, 4)
e.判断 key 是否存在
// 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key] v3, ok := m[3] fmt.Println(ok) fmt.Println(v3) v5, ok := m[5] fmt.Println(ok) fmt.Println(v5)
map
的下标读取可以返回两个值,第一个值为当前 key
的 value
值,第二个值表示对应的 key
是否存在,若存在 ok
为 true
,若不存在,则 ok
为 false
。
f.遍历 map
// 遍历 map 中所有的元素需要用 for range 循环。 for key, value := range m { fmt.Printf("key: %s, value: %s\n", key, value) }
g.获取 map 长度
// 使用 len 函数可以获取 map 长度 fmt.Println(len(m)) // 4
四、range关键字
range 是一个关键字,用于迭代数组、切片、映射、通道或字符串中的元素。
range 的使用方式取决于所遍历的数据类型。对于一个 slice 或者一个 map,可以用 range 来快速遍历,这样代码能够更加简洁。
比如 range 遍历数组或slice的时候,会返回两个值,第一个是索引,第二个是对应位置的值;遍历map也会返回key和value两个值。如果不需要索引,可以用下划线来忽略。
package main import "fmt" func main() { nums := []int{2, 3, 4} sum := 0 for i, num := range nums { sum += num if num == 2 { fmt.Println("index:", i, "num:", num) // index: 0 num: 2 } } fmt.Println(sum) // 9 m := map[string]string{"a": "A", "b": "B"} for k, v := range m { fmt.Println(k, v) // b 8; a A } for k := range m { fmt.Println("key", k) // key a; key b } }
以下是 range
在不同数据类型中的使用示例:
1. 数组和切片
nums := []int{2, 3, 4} // 使用 range 遍历切片 for index, value := range nums { fmt.Printf("Index: %d, Value: %d\n", index, value) }
2. Map
person := map[string]int{"Alice": 25, "Bob": 30} // 使用 range 遍历映射 for key, value := range person { fmt.Printf("Name: %s, Age: %d\n", key, value) }
3. Channel
ch := make(chan int) // 使用 range 遍历通道,等待通道关闭 go func() { for num := range ch { fmt.Println("Received:", num) } }() ch <- 1 ch <- 2 close(ch) // 关闭通道
4. 字符串
在遍历字符串时,value
会表示当前字符的 Unicode 码点值(即“char”)。
text := "Hello, Go!" // 使用 range 遍历字符串 for index, char := range text { fmt.Printf("Index: %d, Char: %c\n", index, char) }
输出:
在这些示例中,range
的语法是相同的:
for index, value := range collection
其中,index 是当前迭代的索引(或键),value 是当前元素的值。
需要注意的是,在使用 range 迭代切片、数组、映射和通道时,会为每个迭代创建一个新的变量副本,而不是直接访问原始数据。这对于遍历数据结构并进行操作是很有用的。
五、函数
这个是 Golang 里面一个简单的实现两个变量相加的函数。 Golang 和其他很多语言不一样的是,变量类型是后置的。
package main import "fmt" func add(a int, b int) int { return a + b } func add2(a, b int) int { return a + b } func exists(m map[string]string, k string) (v string, ok bool) { v, ok = m[k] return v, ok } func main() { res := add(1, 2) fmt.Println(res) // 3 v, ok := exists(map[string]string{"a": "A"}, "a") fmt.Println(v, ok) // A True }
Golang 里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息。
六、指针
Go里面也支持指针。但是,相比 C 和 C++ 里面的指针,支持的操作很有限。指针的一个主要用途就是对于传入参数进行修改。
package main import "fmt" func add2(n int) { n += 2 } func add2ptr(n *int) { *n += 2 } func main() { n := 5 add2(n) fmt.Println(n) // 5 add2ptr(&n) fmt.Println(n) // 7 }package main import "fmt" type user struct { name string password string } func main() { a := user{name: "wang", password: "1024"} b := user{"wang", "1024"} c := user{name: "wang"} c.password = "1024" var d user d.name = "wang" d.password = "1024" fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024} fmt.Println(checkPassword(a, "haha")) // false fmt.Println(checkPassword2(&a, "haha")) // false } func checkPassword(u user, password string) bool { return u.password == password } func checkPassword2(u *user, password string) bool { return u.password == password }
这个函数试图把一个变量+2。但是单纯像上面add2()这种写法其实是无效的,因为传入函数的参数实际上是一个拷贝。add2()中的这个n+=2,是对原变量n的拷贝进行了+2,回到main()中并不起作用。如果想要在函数中对外部变量的修改起作用的话,那么我们需要把那个类型写成指针类型。
为了类型匹配,调用的时候会加一个 & 符号。
七、结构体
结构体是带类型的字段的集合。
package main import "fmt" type user struct { name string password string } func main() { //可以用结构体的名称去初始化一个结构体变量,构造的时候需要传入每个字段的初始值 a := user{name: "wang", password: "1024"} b := user{"wang", "1024"} c := user{name: "wang"} c.password = "1024" //也可以用这种键值对的方式去指定初始值,这样可以只对一部分字段进行初始化 var d user d.name = "wang" d.password = "1024" fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024} fmt.Println(checkPassword(a, "haha")) // false fmt.Println(checkPassword2(&a, "haha")) // false } func checkPassword(u user, password string) bool { return u.password == password } func checkPassword2(u *user, password string) bool { return u.password == password }
比如这里 user 结构体包含了两个字段,name 和 password。
同样的,结构体也能支持指针,这样能够实现对于结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。
结构体方法
在 Golang 里,可以为结构体定义一些方法。
结构体方法是与特定类型的结构体相关联的函数。结构体方法允许为结构体类型定义“附加的功能”,并且可以通过结构体实例调用这些方法。这在面向对象编程中类似于类的方法。
要定义一个结构体方法,需要:
先定义一个结构体类型。
然后,为该结构体类型定义一个方法。
比如将上面例子中的 checkPassword()从一个普通函数改成结构体方法。这样用户可以通过 a.checkPassword(“xx”) 这样去调用。
具体的代码修改,就是把第一个参数,加上括号,写到函数名称前面。在实现结构体的方法的时候也有两种写法,一种是带指针,一种是不带指针。它们的区别是,如果带指针的话,那么就可以对这个结构体去做修改。如果不带指针的话,那实际上操作的是一个拷贝,就无法对结构体进行修改。
package main import "fmt" type user struct { name string password string } func (u user) checkPassword(password string) bool { return u.password == password } func (u *user) resetPassword(password string) { u.password = password } func main() { a := user{name: "wang", password: "1024"} a.resetPassword("2048") fmt.Println(a.checkPassword("2048")) // true }
以下例子演示了如何在 Go 中定义和使用结构体方法:
package main import ( "fmt" ) // 定义一个结构体类型 type Rectangle struct { Width float64 Height float64 } // 为 Rectangle 结构体定义一个方法 func (r Rectangle) Area() float64 { return r.Width * r.Height } func main() { // 创建一个 Rectangle 结构体实例 rect := Rectangle{Width: 10, Height: 5} // 调用结构体方法 area := rect.Area() fmt.Println("矩形的面积:", area) }
在上述示例中,我们定义了一个名为 Rectangle
的结构体类型,具有 Width
和 Height
两个字段。然后,我们为 Rectangle
结构体定义了一个名为 Area
的方法,用于计算矩形的面积。
结构体方法的语法如下:
func (receiver Type) MethodName() ReturnType { // 方法实现 }
receiver:是方法的接收器,它定义了哪个结构体类型可以调用该方法。在上面的例子中,receiver 是 Rectangle 结构体类型。
MethodName:是为该结构体定义的方法的名称。
ReturnType:是该方法返回的数据类型。
结构体方法在 Go 中被广泛使用,用于将操作与数据结构关联起来,提高代码的可读性和封装性。通过使用方法,可以将特定类型的功能封装到结构体中,并通过结构体实例调用这些方法来执行相关操作。
【字节跳动青训营】后端笔记整理-1 | Go语言入门指南:基础语法和常用特性解析(三)
+https://developer.aliyun.com/article/1521863?spm=a2c6h.13148508.setting.17.439a4f0eAvjRuU