【字节跳动青训营】后端笔记整理-1 | Go语言入门指南:基础语法和常用特性解析(二)

简介: Go 语言中的复合数据类型包括数组、切片(slice)、映射(map)和结构体(struct)。

【字节跳动青训营】后端笔记整理-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 的结构体类型,具有 WidthHeight 两个字段。然后,我们为 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

相关文章
|
2天前
|
Go Python
go语言调用python脚本
go语言调用python脚本
4 0
|
4天前
|
负载均衡 算法 Java
【面试宝藏】Go语言运行时机制面试题
探索Go语言运行时,了解goroutine的轻量级并发及GMP模型,包括G(协程)、M(线程)和P(处理器)。GMP调度涉及Work Stealing和Hand Off机制,实现负载均衡。文章还讨论了从协作到基于信号的抢占式调度,以及GC的三色标记算法和写屏障技术。理解这些概念有助于优化Go程序性能。
23 4
|
5天前
|
JSON Go 数据格式
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(4)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
5天前
|
Java 编译器 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(3)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
5天前
|
存储 安全 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(2)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
5天前
|
Java Go 索引
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(1)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
5天前
|
安全 Go 开发者
Go语言中的空值与零值有什么区别?
在Go语言中,`nil`和零值有显著区别。`nil`用于表示指针、通道等类型的“无”或“不存在”,而零值是类型的默认值,如数字的0,字符串的`&#39;&#39;`。`nil`常用于未初始化的变量或错误处理,零值用于提供初始值和避免未初始化的使用。理解两者差异能提升代码质量和稳定性。
|
7天前
|
Go
如何理解Go语言中的值接收者和指针接收者?
Go语言中,函数和方法可使用值或指针接收者。值接收者是参数副本,内部修改不影响原值,如示例中`ChangeValue`无法改变`MyStruct`的`Value`。指针接收者则允许修改原值,因为传递的是内存地址。选择接收者类型应基于是否需要修改参数,值接收者用于防止修改,指针接收者用于允许修改。理解这一区别对编写高效Go代码至关重要。
|
8天前
|
缓存 Java Go
如何用Go语言构建高性能服务
【6月更文挑战第8天】Go语言凭借其并发能力和简洁语法,成为构建高性能服务的首选。本文关注使用Go语言的关键设计原则(简洁、并发、错误处理和资源管理)、性能优化技巧(减少内存分配、使用缓存、避免锁竞争、优化数据结构和利用并发模式)以及代码示例,展示如何构建HTTP服务器。通过遵循这些原则和技巧,可创建出稳定、高效的Go服务。
|
9天前
|
存储 NoSQL Go
轻松上手,使用Go语言操作Redis数据库
轻松上手,使用Go语言操作Redis数据库

推荐镜像

更多