通过示例学习-Go-语言-2023-十六-

通过示例学习 Go 语言 2023(十六)

Golang 正则表达式:理解正则表达式中的花括号

来源:golangbyexample.com/curly-braces-regex-golang/

目录

  • 概述

  • 示例

  • 花括号应用于分组

  • 花括号应用于字符类

  • 如何在正则表达式中将花括号作为字面字符使用。

概述

花括号在正则表达式中作为重复量词。它们指定前面字符在输入字符串或文本中可以出现的次数。它们也可以用于指定范围,即指定字符出现的最小和最大次数。

它的语法是

{min, max}

其中

  • min表示字符可以出现的最小次数

  • max表示字符可以出现的最大次数

例如

a{n}

这指定字符“a”可以恰好出现 n 次。类似地,对于以下正则表达式

\d{n}

这指定任何数字可以恰好出现 n 次。花括号也可以用于定义范围。

例如

  • {m,n} – 至少m次,最多n

  • {m, } – 至少m

  • {, n} – 最多n

让我们看一个相同的例子

示例

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(`b{2}`)

	matches := sampleRegexp.FindString("bb")
	fmt.Println(matches)

	matches = sampleRegexp.FindString("bbb")
	fmt.Println(matches)

	matches = sampleRegexp.FindString("bbbb")
	fmt.Println(matches)
}

输出

bb
bb
bb

默认情况下,花括号是贪婪或非懒惰的。这意味着什么?它们会匹配所有可能的字符,并且总是更倾向于更多。也可以使花括号操作符非贪婪或懒惰。这可以通过在花括号操作符后添加问号来实现。让我们看一个相同的例子。

从输出中可以看到,在花括号操作符后添加问号操作符后,它尝试匹配尽可能少的字符,即变得非贪婪。

这就是给定正则表达式的原因

ab{2,4}

对于以下所有输入字符串,它给出匹配abb

abb
abbb
abbbb

相同的程序

package main

import (
    "fmt"
    "regexp"
)

func main() {
    sampleRegexp := regexp.MustCompile(`ab{2,4}`)

    matches := sampleRegexp.FindStringSubmatch("abb")
    fmt.Println(matches)

    matches = sampleRegexp.FindStringSubmatch("abbb")
    fmt.Println(matches)

    matches = sampleRegexp.FindStringSubmatch("abbbb")
    fmt.Println(matches)
}

输出

abb
abbb
abbbb

同时

ab{2,4}? 对于上述所有输入字符串,将始终给出匹配abb

相同的程序

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(`ab{2,4}?`)

	matches := sampleRegexp.FindString("abb")
	fmt.Println(matches)

	matches = sampleRegexp.FindString("abbb")
	fmt.Println(matches)

	matches = sampleRegexp.FindString("abbbb")
	fmt.Println(matches)
}

输出

abb
abb
abb

花括号应用于分组

正则表达式的一部分可以放置在平衡括号内。这部分现在是一个组。我们还可以将花括号应用于此组。花括号将在分组后添加。

让我们看一个相同的例子。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(`(ab){2}`)

	matches := sampleRegexp.FindString("abab")
	fmt.Println(matches)

	matches = sampleRegexp.FindString("ababbc")
	fmt.Println(matches)
}

输出

abab
abab

花括号应用于字符类

花括号量词也可以应用于整个字符类。其含义保持不变。字符类在正则表达式中由方括号表示。让我们看一个相同的程序。

在上述程序中,我们有以下正则表达式

[ab]{4}

这意味着它将匹配一个长度恰好为 4 并由字符‘a’‘b’以任何顺序组成的字符串

这就是为什么正则表达式匹配下面的字符串

abab
aaaa
bbbb
aabb
bbaa

而且它不匹配

aba - String of length 3
abbaa - String of length 5

如何在正则表达式中将大括号用作字面字符。

转义字符可以放在开括号或闭合括号之前,如果需要以字面方式使用它们。

如果一个闭合括号前没有开括号,它会被视为字面意义上的闭合括号。

这就是在 Golang 中正则表达式中大括号的所有内容。希望你喜欢这篇文章。请在评论中分享反馈

注意: 请查看我们的 Golang 高级教程。本系列的教程内容详尽,我们尽力覆盖所有概念并提供示例。这个教程适合那些希望获得专业知识和对 Golang 有深入理解的人 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,这篇文章适合你 – 所有设计模式 Golang

Golang 正则表达式:理解点字符‘.’。

来源:golangbyexample.com/dot-chracter-golang-regex/

目录

  • 概述

    • MatchCompile 函数

    • 匹配方法

  • 将点作为字面字符使用

  • 字符类中的点字符

概述

点字符‘.’是正则表达式中最常用的元字符之一。它用于匹配任何字符。如果在正则表达式中添加特定的标志,它也可以匹配新行,稍后我们会讨论这个。默认情况下,它不匹配新行。

在查看正则表达式本身和点字符‘.’的用法之前,让我们看一下 Go 提供的一些基本函数或方法来进行正则匹配。

MatchCompile 函数

golang.org/pkg/regexp/#MustCompile。以下是该函数的签名。

func MustCompile(str string) *Regexp

我们首先使用MustCompile函数编译给定的正则表达式字符串。如果给定的正则表达式无效,该函数会引发错误。在成功编译给定的正则表达式后,它返回regexp结构的实例。

sampleRegexp := regexp.MustCompile("some_regular_expression")

匹配方法

golang.org/pkg/regexp/#Regexp.Match

以下是该方法的签名。

func (re *Regexp) Match(b []byte) bool

我们可以在regexp结构实例上调用Match方法,将给定模式与正则表达式匹配。如果正则表达式与输入字符串匹配,则返回 true,否则返回 false。我们需要将输入字符串的字节传递给此方法。

match := sampleRegexp.Match([]byte("some_string"))

稍后我们将在示例中看到这两个函数的实际应用。

现在让我们看一个简单的点字符‘.’程序。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(".")

	match := sampleRegexp.Match([]byte("a"))
	fmt.Printf("For a: %t\n", match)

	match = sampleRegexp.Match([]byte("b"))
	fmt.Printf("For b: %t\n", match)

	match = sampleRegexp.Match([]byte("ab"))
	fmt.Printf("For ab: %t\n", match)

	match = sampleRegexp.Match([]byte(""))
	fmt.Printf("For empty string: %t\n", match)
}

输出

For a: true
For b: true
For ab: true
For empty string: false

在上述程序中,我们有一个仅包含一个点字符的简单正则表达式。

sampleRegexp := regexp.MustCompile(".")

它匹配以下字符和字符串。

a
b
ab

它匹配ab,因为默认情况下正则表达式不会匹配完整字符串,除非我们使用锚字符(插入符号和美元符号)。这就是它在‘ab’中匹配第一个字符‘a’并报告匹配的原因。

它不匹配空字符串。

让我们看另一个示例,其中正则表达式中有两个点。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile("..")
	match := sampleRegexp.Match([]byte("ab"))
	fmt.Printf("For ab: %t\n", match)

	match = sampleRegexp.Match([]byte("ba"))
	fmt.Printf("For ba: %t\n", match)

	match = sampleRegexp.Match([]byte("abc"))
	fmt.Printf("For abc: %t\n", match)

	match = sampleRegexp.Match([]byte("a"))
	fmt.Printf("For a: %t\n", match)
}

输出

For ab: true
For ba: true
For abc: true
For a: false

在上述程序中,我们有一个包含两个点的简单正则表达式。

sampleRegexp := regexp.MustCompile("..")

它将匹配包含至少两个字符的给定字符串作为子字符串。

这就是它给出匹配的原因。

ab
ba
abc

并且不匹配

a

如前所述,点‘.’也不匹配新行。但可以通过在正则表达式的开头添加一组标志来改变默认行为。我们需要添加到正则表达式开头的标志是:

(?s)

让我们看一个相同的程序。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(".")

	match := sampleRegexp.Match([]byte("\n"))
	fmt.Printf("For \\n: %t\n", match)

	sampleRegexp = regexp.MustCompile("(?s).")

	match = sampleRegexp.Match([]byte("\n"))
	fmt.Printf("For \\n: %t\n", match)
}

输出

For \n: false
For \n: true
sampleRegexp := regexp.MustCompile(".")

sampleRegexp = regexp.MustCompile("(?s).")

在第二个正则表达式中,我们添加了额外的标志。这就是为什么它能够匹配换行符,而第一个没有标志的正则表达式则无法匹配。

将点作为字面字符使用

如果你想将点‘.’作为字面字符使用,我们需要用反斜杠进行转义。转义后,它将匹配一个字面点字符。例如,如果我们想匹配以下的字面字符串或文本。

a.b

那么相应的正则表达式将是。

a\.b

这里是相应的程序。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile("a\\.b")

	match := sampleRegexp.Match([]byte("a.b"))

	fmt.Printf("For a.b string: %t\n", match)
}

输出

For a.b string: true

字符类中的点字符

点或‘.’在方括号或字符类内被视为字面字符。在那里面不需要转义。让我们看看一个相应的工作程序。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile("[.]")
	match := sampleRegexp.Match([]byte("."))

	fmt.Println(match)

}

输出

true
Also, check out our Golang advance tutorial Series – Golang Advance Tutorial

Golang 正则表达式:在正则表达式中使用变量

来源:golangbyexample.com/variable-regex-golang/

目录

  • 概述

  • 程序

概述

regexp.MustCompile 函数用于编译给定的正则表达式字符串。因此,传递给 MustCompile 函数的仅是一个字符串。由于它是一个字符串,我们可以将任何变量与其余模式连接起来。

例如

regex := `b+`
sampleRegexp := regexp.MustCompile("a" + regex)

因此,在这里我们正在进行连接以获取整个模式。

"a" + regex

让我们来看一个运行程序。

程序

package main

import (
	"fmt"
	"regexp"
)

func main() {
	regex := `b+`
	sampleRegexp := regexp.MustCompile("a" + regex)

	match := sampleRegexp.FindString("abb")
	fmt.Println(match)

}

输出

abb

Golang 正则匹配任何字符

来源:golangbyexample.com/golang-regex-match-any-character/

目录

  • 概述

  • 程序

概述

点 ‘.’ 字符是正则表达式中最常用的元字符之一。它用于匹配任何字符。默认情况下,它不匹配新行。

程序

现在让我们看看一个简单的点 ‘.’ 字符程序

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(".")

	match := sampleRegexp.Match([]byte("a"))
	fmt.Printf("For a: %t\n", match)

	match = sampleRegexp.Match([]byte("b"))
	fmt.Printf("For b: %t\n", match)

	match = sampleRegexp.Match([]byte("ab"))
	fmt.Printf("For ab: %t\n", match)

	match = sampleRegexp.Match([]byte(""))
	fmt.Printf("For empty string: %t\n", match)
}

输出

For a: true
For b: true
For ab: true
For empty string: false

在上述程序中,我们有一个简单的正则表达式,仅包含一个点字符。

sampleRegexp := regexp.MustCompile(".")

它匹配下面的字符和字符串。

a
b
ab

它匹配 ab,因为默认情况下正则表达式不会匹配完整字符串,除非我们使用锚字符(插入符号和美元字符)。这就是它匹配字符串 ‘ab’ 中第一个字符 ‘a’ 并报告匹配的原因。

它不匹配空字符串。

让我们看看另一个例子,其中正则表达式中有两个点。

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile("..")
	match := sampleRegexp.Match([]byte("ab"))
	fmt.Printf("For ab: %t\n", match)

	match = sampleRegexp.Match([]byte("ba"))
	fmt.Printf("For ba: %t\n", match)

	match = sampleRegexp.Match([]byte("abc"))
	fmt.Printf("For abc: %t\n", match)

	match = sampleRegexp.Match([]byte("a"))
	fmt.Printf("For a: %t\n", match)
}

输出

For ab: true
For ba: true
For abc: true
For a: false

在上述程序中,我们有一个简单的正则表达式,包含两个点。

sampleRegexp := regexp.MustCompile("..")

它将匹配任何包含至少两个字符作为子字符串的给定字符串。

这就是为什么它给出匹配的原因

ab
ba
abc

并且不匹配

a

‘.’ 如前所述,不匹配新行。但默认行为可以通过在正则表达式开头添加一组标志来更改。我们需要添加到正则表达式开头的标志是:

(?s)

让我们看看相同的程序

package main

import (
	"fmt"
	"regexp"
)

func main() {
	sampleRegexp := regexp.MustCompile(".")

	match := sampleRegexp.Match([]byte("\n"))
	fmt.Printf("For \\n: %t\n", match)

	sampleRegexp = regexp.MustCompile("(?s).")

	match = sampleRegexp.Match([]byte("\n"))
	fmt.Printf("For \\n: %t\n", match)
}

输出

For \n: false
For \n: true
sampleRegexp := regexp.MustCompile(".")

sampleRegexp = regexp.MustCompile("(?s).")

在第二个正则表达式中,我们添加了额外的标志。这就是为什么它在新行上给出匹配,而第一个没有标志的正则表达式不匹配的原因。

Go 中的协程(Golang)

来源:golangbyexample.com/goroutines-golang/

这是 golang 综合教程系列的第二十三章。有关该系列其他章节,请参考此链接 – Golang 综合教程系列

下一个教程 – 通道

上一个教程 – Iota

现在让我们查看当前教程。以下是当前教程的目录。

目录

  • 概述

  • 启动一个 go 协程

  • 主协程

  • 创建多个协程

  • 协程调度

    • 本地运行队列

    • 全局运行队列

  • Golang 调度器是一个协作调度器

  • 协程相较于线程的优势

  • 匿名协程

  • 结论

概述

协程可以被视为一种轻量级线程,具有独立的执行,并可以与其他协程并发执行。它是一个与其他协程并发执行的函数或方法。它完全由 GO 运行时管理。Golang 是一种并发语言。每个协程都是一个独立的执行。正是协程帮助在 golang 中实现并发。

启动一个 go 协程

Golang 使用特殊关键字‘go’来启动协程。只需在函数或方法调用之前添加go关键字即可启动一个。该函数或方法现在将在协程中执行。请注意,并不是函数或方法决定是否是协程。如果我们用 go 关键字调用该方法或函数,那么该函数或方法就被认为是在协程中执行。

让我们理解正常运行函数与将函数作为协程运行之间的区别。

  • 正常运行一个函数
statment1
start()
statement2

在上述场景中正常运行函数。

  1. 首先,将执行statement1

  2. 然后将调用start()函数

  3. 一旦 start()函数完成,statement2将被执行

  • 将函数作为协程运行
statment1
go start()
statement2

在上述场景中将函数作为协程运行

  1. 首先,将执行 statement1

  2. 然后将作为协程调用 start()函数,该函数将异步执行。

  3. statement2将立即执行。它不会等待start()函数完成。开始函数将作为协程并发执行,而程序的其余部分将继续执行。

所以基本上,当以协程方式调用一个函数时,调用将立即返回,执行将从下一行继续,而协程将在后台并发执行。此外,请注意,从协程返回的任何值都将被忽略。

让我们看一个程序以理解上述观点。

package main

import (
    "fmt"
    "time"
)

func main() {
    go start()
    fmt.Println("Started")
    time.Sleep(1 * time.Second)
    fmt.Println("Finished")
}

func start() {
    fmt.Println("In Goroutine")
}

输出

Started
In Goroutine
Finished

在上述程序中,我们在函数调用前使用“go”关键字来启动协程。

go start()

上面的代码将启动一个协程,该协程将运行start()函数。程序首先打印“Started”。注意打印“Started”的行是在协程启动之后。这说明了上述提到的,在协程启动后调用会从下一行继续。然后我们设置一个超时。超时的目的是确保协程在主协程退出之前被调度。因此,现在协程执行并打印。

In Goroutine

然后它打印。

Finished

当我们移除超时时,会发生什么?让我们看一个程序。

package main
import (
    "fmt"
)
func main() {
    go start()
    fmt.Println("Started")
    fmt.Println("Finished")
}
func start() {
    fmt.Println("In Goroutine")
}

输出

Started
Finished

上面的程序从未打印。

In Goroutine

这意味着协程从未被执行。这是因为主协程或程序在协程被调度之前就退出了。这引发了关于主协程的讨论。

主协程

main包中的main函数是主协程。所有协程都是从主协程启动的。这些协程可以再启动多个其他协程,依此类推。

主协程代表主程序。一旦它退出,就意味着程序已退出。

协程没有父子关系。当你启动一个协程时,它只是与所有其他正在运行的协程并行执行。每个协程仅在其函数返回时退出。唯一的例外是所有协程在主协程(运行main函数的那个)退出时也会退出。

让我们看一个程序来演示协程没有父子关系。

package main

import (
    "fmt"
    "time"
)

func main() {
    go start()
    fmt.Println("Started")
    time.Sleep(1 * time.Second)
    fmt.Println("Finished")
}

func start() {
    go start2()
    fmt.Println("In Goroutine")
}
func start2() {
    fmt.Println("In Goroutine2")
}

输出

Started
In Goroutine
In Goroutine2
Finished

在上述程序中,第一个协程启动第二个协程。第一个协程然后打印“In Goroutine”,随后退出。第二个协程开始并打印“In Goroutine2”。这表明协程没有父子关系,它们作为独立的执行存在。

此外,请注意,超时只是为了说明,不应在生产环境中使用。

创建多个协程

让我们看下面的程序以启动多个协程。这个例子也将演示协程是并发执行的。

package main

import (
    "fmt"
    "time"
)

func execute(id int) {
    fmt.Printf("id: %d\n", id)
}

func main() {
    fmt.Println("Started")
    for i := 0; i < 10; i++ {
        go execute(i)
    }
    time.Sleep(time.Second * 2)
    fmt.Println("Finished")
}

输出

Started
id: 4
id: 9
id: 1
id: 0
id: 8
id: 2
id: 6
id: 3
id: 7
id: 5
Finished

上述程序将在循环中生成 10 个协程。每次运行程序时,它都会给出不同的输出,因为协程将并发运行,无法确定哪个会先运行。

让我们了解 Go 调度器的工作原理。理解协程后将更容易。

协程的调度

一旦 Go 程序启动,Go 运行时将启动与当前进程可用的逻辑 CPU 数量相等的操作系统线程。每个虚拟核心有一个逻辑 CPU,其中虚拟核心的含义是

virtual_cores = x*number_of_physical_cores

其中 x=每个核心的硬件线程数量

runtime.Numcpus函数可用于获取可用于 Go 程序的逻辑处理器数量。见下面的程序

package main
import (
    "fmt"
    "runtime"
)
func main() {
    fmt.Println(runtime.NumCPU())
}

在我的机器上显示 16。我的机器有 8 个物理核心,每个核心有 2 个硬件线程。因此 2*8=16。

Go 程序将启动与可用逻辑 CPU 数量相等的操作系统线程,或运行时的runtime.NumCPU()输出。这些线程将由操作系统管理,这些线程调度到 CPU 核心的责任仅在于操作系统。

Go 运行时有自己的调度器,它将在 Go 运行时的操作系统级线程上复用协程。因此,本质上每个协程在分配给逻辑 CPU 的操作系统线程上运行。

管理协程并将其分配给操作系统线程涉及两个队列

本地运行队列

在 Go 运行时,每个操作系统线程都有一个关联的队列。称为本地运行队列。它包含在该线程上下文中执行的所有协程。Go 运行时将调度并进行上下文切换,将属于特定 LRQ 的协程分配到拥有该 LRQ 的相应操作系统线程。

全局运行队列

它包含所有尚未移动到任何操作系统线程 LRQ 的协程。Go 调度器将从此队列中将一个协程分配到任何操作系统线程的本地运行队列。

下面的图示说明了调度器的工作原理。

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/ddadf96d5c501fe34af12075fcb1e18d.png)

Golang 调度器是一个协作调度器

Go 调度器是一个协作调度器。这意味着它是非抢占式的。没有基于时间的抢占,这是抢占式调度器的情况。在协作调度器中,线程必须明确让出执行。协程可以在某些特定检查点让出其执行给其他协程。

运行时在函数调用时调用调度器,以决定是否需要调度一个新的协程。因此,当一个协程进行任何函数调用时,调度器将被调用,可能发生上下文切换,这意味着可能调度一个新的协程。现有的协程也可能继续执行。调度器还可以在以下事件中进行上下文切换。

  1. 函数调用

  2. 垃圾回收

  3. 网络调用

  4. 通道操作

  5. 使用 go 关键字时

  6. 在原语(如互斥锁等)上阻塞

需要提到的是,调度器在上述事件中运行,但这并不意味着上下文切换会发生。只是调度器获得了机会。是否进行上下文切换则取决于调度器。

goroutines 相对于线程的优势

  • Goroutines 的初始大小为 8kb,其大小可以根据运行时需求进行增长或缩小。而操作系统线程的大小则超过 1 mb。因此,分配 goroutines 的成本极低。这样可以同时启动大量 goroutines。goroutine 的增长和缩小由 Go 运行时内部管理。由于 goroutines 成本低,你可以启动成千上万的 goroutines,而线程则只能启动几千个。

  • goroutine 的调度由 Go 运行时管理。如上所述,Go 运行时内部启动与逻辑 CPU 数量相等的操作系统线程。然后,它将 goroutines 重新调度到每个操作系统线程上。因此,goroutines 的调度由 Go 运行时完成,因此速度非常快。在线程的情况下,线程的调度由操作系统运行时完成。因此,goroutines 的上下文切换时间远快于线程的上下文切换时间。因此,成千上万的 goroutines 被多路复用在一两个操作系统线程上。如果你在 JAVA 中启动 1000 个线程,它将消耗大量资源,这 1000 个线程需要由操作系统管理。此外,这些线程的大小都将超过 1 MB。

  • Goroutines 通过内置的原始 channel 进行通信,这些 channel 是为处理竞争条件而设计的。因此,goroutines 之间的通信是安全的,并且避免了显式锁定。所以在 goroutines 之间共享的数据结构不需要被锁定。多线程编程需要使用锁来访问共享变量,这可能导致死锁和竞争条件,且难以检测。相比之下,goroutines 使用 channel 进行通信,整个同步由 Go 运行时管理。这样就避免了死锁和竞争条件。实际上,Go 信奉这样一个口号

"Don't share memory for communication, instead share memory by communicating"

匿名 goroutines

在 Golang 中,匿名函数也可以通过 goroutine 调用。有关匿名函数的更多理解,请参考这篇文章 - golangbyexample.com/go-anonymous-function/

下面是调用匿名函数在 goroutine 中的格式

go func(){
   //body
}(args..)

使用 goroutine 调用匿名函数和调用普通函数时,行为没有区别。

让我们看一个例子:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("In Goroutine")
    }()

    fmt.Println("Started")
    time.Sleep(1 * time.Second)
    fmt.Println("Finished")
}

输出

Started
In Goroutine
Finished

结论

这就是关于 Golang 中 goroutines 的所有内容。希望你喜欢这个教程。请在评论中分享反馈/改进/错误。

下一个教程Channel

上一个教程Iota

Go 中的 Goto 语句

来源:golangbyexample.com/goto-statement-go/

目录

  • 概述

  • 示例

概述

Goto 语句允许无条件跳转到同一函数中的标记语句。下面是 goto 语句的格式

goto label
...
...
label: statement

标签可以是有效的 go 语句,不能是关键字。一旦遇到 goto 语句,控制就会转移到指定的标签,并从那里继续执行。标签仅在声明它的函数内部可见,函数外部的任何引用都会导致编译错误。

示例

让我们看看一个简单的 goto 语句示例

package main
import "fmt"
func main() {
    learnGoTo()
}
func learnGoTo() {
    fmt.Println("a")
    goto FINISH
    fmt.Println("b")
FINISH:
    fmt.Println("c")
}

输出

a
c

在上面的示例中,我们有一个goto语句,如下所示。

goto FINISH

FINISH标签如下

FINISH:
    fmt.Println("c")

一旦程序遇到goto语句,它就会跳转到指定的标签。因此,下面的代码行永远不会被执行,b也不会被打印。

fmt.Println("b")

标签和 goto 需要属于同一函数,否则会引发编译错误。这是因为标签的作用域在声明它的函数内部。如上所述,下面的程序会引发编译错误。

./main.go:11:7: label FINISH not defined
./main.go:17:1: label FINISH defined and not used
package main

import "fmt"

func main() {
	learnGoTo()
}

func learnGoTo() {
	fmt.Println("a")
	goto FINISH
	fmt.Println("b")

}

func test() {
FINISH:
	fmt.Println("c")
}

标签也可以在 goto 语句之前。见下面的示例。该程序可以用于打印 10 之前的所有奇数。注意标签START在这里位于 goto 之前。

package main
import "fmt"
func main() {
    learnGoTo()
}
func learnGoTo() {
    i := 0
    fmt.Println("here")
START:
    for i < 10 {
        if i%2 == 0 {
            i++
            goto START
        }  
        fmt.Println(i)
        i++
    }
}

不建议使用 Goto,因为可读性差,通常也是许多错误的来源。任何通过 goto 实现的功能都可以通过其他 go 结构来实现。

在 Go (Golang)中将字母异位词组合在一起的程序

来源:golangbyexample.com/group-anagrams-together-go/

目录

  • 概述

  • 程序

概述

给定一个字符串数组,编写一个程序将所有字母异位词组合在一起。来自维基百科

字母异位词是通过重新排列不同单词或短语的字母形成的单词或短语,通常使用所有原始字母一次。例如,单词anagram本身可以重新排列为nagaram,单词binary可以变为brainy^,单词adobe可以变为abode

例如

Input: ["art", "tap", "rat", "pat", "tar","arm"]
Output: [["art", "rat", "tar"], ["tap", "pat"], ["arm"]]

以下是策略。

  • 复制原始数组。对副本数组中的每个字符串进行排序。排序后的副本数组将如下所示
["art", "apt", "art", "apt", "art", "arm"]
  • 创建一个映射以存储输出
var output map[string][]int
  • 为上述副本数组构建一个前缀树,所有字符串都已排序。插入每个元素后更新上述映射。对于“art”,映射应如下所示,因为 art 在原始数组中的位置为 0、2 和 5。
map["art"] = [0,2,4]
  • 遍历映射并通过在输入字符串数组中索引打印输出

程序

以下是相应的程序

package main

import (
	"fmt"
	"sort"
)

func main() {
	strs := []string{"art", "tap", "rat", "pat", "tar", "arm"}
	output := groupAnagrams(strs)
	fmt.Println(output)

	strs = []string{""}
	output = groupAnagrams(strs)
	fmt.Println(output)

	strs = []string{"a"}
	output = groupAnagrams(strs)
	fmt.Println(output)
}

type sortRune []rune

func (s sortRune) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

func (s sortRune) Less(i, j int) bool {
	return s[i] < s[j]
}

func (s sortRune) Len() int {
	return len(s)
}

func groupAnagrams(strs []string) [][]string {

	anagramMap := make(map[string][]int)
	var anagrams [][]string
	trie := &trie{root: &trieNode{}}

	lenStrs := len(strs)

	var strsDup []string

	for i := 0; i < lenStrs; i++ {
		runeCurrent := []rune(strs[i])
		sort.Sort(sortRune(runeCurrent))
		strsDup = append(strsDup, string(runeCurrent))
	}

	for i := 0; i < lenStrs; i++ {
		anagramMap = trie.insert(strsDup[i], i, anagramMap)
	}

	for _, value := range anagramMap {
		var combinedTemp []string
		for i := 0; i < len(value); i++ {
			combinedTemp = append(combinedTemp, strs[value[i]])
		}
		anagrams = append(anagrams, combinedTemp)
	}

	return anagrams
}

type trieNode struct {
	isWord    bool
	childrens [26]*trieNode
}

type trie struct {
	root *trieNode
}

func (t *trie) insert(input string, wordIndex int, anagramMap map[string][]int) map[string][]int {
	inputLen := len(input)
	current := t.root

	for i := 0; i < inputLen; i++ {
		index := input[i] - 'a'
		if current.childrens[index] == nil {
			current.childrens[index] = &trieNode{}
		}
		current = current.childrens[index]
	}
	current.isWord = true
	if anagramMap[input] == nil {
		anagramMap[input] = []int{wordIndex}
	} else {
		anagramMap[input] = append(anagramMap[input], wordIndex)
	}
	return anagramMap
}

输出

[[art rat tar] [tap pat] [arm]]
[[]]
[[a]]

注意: 查看我们的 Golang 高级教程。本系列的教程内容详尽,我们尝试覆盖所有概念及示例。本教程适合希望获得专业知识和对 golang 有深入理解的学习者 - Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,这篇文章适合你 - 所有设计模式 Golang

Golang 中的堆

来源:golangbyexample.com/heap-in-golang/

目录

** 介绍

  • MinHeap 的实现

介绍

堆是一种完整的二叉树。完整的二叉树是指除最后一层外,所有层都是满的。堆有两种类型:

  • MinHeap:MinHeap 是一种完整的二叉树,其中父节点的值小于或等于其左右子节点的值。

  • MaxHeap:MaxHeap 是一种完整的二叉树,其中父节点的值大于或等于其左右子节点的值。

以下是一个 minheap 的表示。注意父节点总是小于或等于子节点

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/693dba4ca31661ce2f15e7001b645441.png)

以下是一个 maxheap 的表示。注意父节点总是大于或等于子节点

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/32fb0e4c70c6a9b75511ddafc2106833.png)

在这篇文章中,让我们来看一下 Go 中 minheap 的实现。

MaxHeap 的实现可以在链接中找到 – golangbyexample.com/maxheap-in-golang/

MinHeap 的完整描述可以在链接中找到 – golangbyexample.com/minheap-in-golang/

MinHeap 的实现

package main

import "fmt"

type minheap struct {
    heapArray []int
    size      int
    maxsize   int
}

func newMinHeap(maxsize int) *minheap {
    minheap := &minheap{
        heapArray: []int{},
        size:      0,
        maxsize:   maxsize,
    }
    return minheap
}

func (m *minheap) leaf(index int) bool {
    if index >= (m.size/2) && index <= m.size {
        return true
    }
    return false
}

func (m *minheap) parent(index int) int {
    return (index - 1) / 2
}

func (m *minheap) leftchild(index int) int {
    return 2*index + 1
}

func (m *minheap) rightchild(index int) int {
    return 2*index + 2
}

func (m *minheap) insert(item int) error {
    if m.size >= m.maxsize {
        return fmt.Errorf("Heal is ful")
    }
    m.heapArray = append(m.heapArray, item)
    m.size++
    m.upHeapify(m.size - 1)
    return nil
}

func (m *minheap) swap(first, second int) {
    temp := m.heapArray[first]
    m.heapArray[first] = m.heapArray[second]
    m.heapArray[second] = temp
}

func (m *minheap) upHeapify(index int) {
    for m.heapArray[index] < m.heapArray[m.parent(index)] {
        m.swap(index, m.parent(index))
    }
}

func (m *minheap) downHeapify(current int) {
    if m.leaf(current) {
        return
    }
    smallest := current
    leftChildIndex := m.leftchild(current)
    rightRightIndex := m.rightchild(current)
    //If current is smallest then return
    if leftChildIndex < m.size && m.heapArray[leftChildIndex] < m.heapArray[smallest] {
        smallest = leftChildIndex
    }
    if rightRightIndex < m.size && m.heapArray[rightRightIndex] < m.heapArray[smallest] {
        smallest = rightRightIndex
    }
    if smallest != current {
        m.swap(current, smallest)
        m.downHeapify(smallest)
    }
    return
}
func (m *minheap) buildMinHeap() {
    for index := ((m.size / 2) - 1); index >= 0; index-- {
        m.downHeapify(index)
    }
}

func (m *minheap) remove() int {
    top := m.heapArray[0]
    m.heapArray[0] = m.heapArray[m.size-1]
    m.heapArray = m.heapArray[:(m.size)-1]
    m.size--
    m.downHeapify(0)
    return top
}

func main() {
    inputArray := []int{6, 5, 3, 7, 2, 8}
    minHeap := newMinHeap(len(inputArray))
    for i := 0; i < len(inputArray); i++ {
        minHeap.insert(inputArray[i])
    }
    minHeap.buildMinHeap()
    for i := 0; i < len(inputArray); i++ {
        fmt.Println(minHeap.remove())
    }
    fmt.Scanln()
}

输出:

2
3
5
6
7
8

Golang 中的堆排序

来源:golangbyexample.com/heapsort-in-golang/

目录

** 介绍

  • 堆排序步骤:

  • 完整工作代码

  • 时间复杂度

介绍

堆排序是一种基于比较的排序算法,它使用堆数据结构。有关堆的更多信息,请参考此链接 – golangbyexample.com/heap-in-golang/

本文演示了使用最小堆进行堆排序。也可以使用最大堆实现相同的功能。

  • 左子节点 – 2*i + 1

  • 右子节点 – 2*i + 2

以下是最小堆的表示

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/693dba4ca31661ce2f15e7001b645441.png)

堆排序步骤:

  • 构建最小堆。在最小堆构建完成后,第一个元素变为最小值

  • 将第一个元素移动到数组的最后。再次调用堆化,大小减一。对数组的大小重复此操作

  • 最终数组从大到小排序

完整工作代码

package main

import "fmt"

type minheap struct {
    arr []int
}

func newMinHeap(arr []int) *minheap {
    minheap := &minheap{
        arr: arr,
    }
    return minheap
}

func (m *minheap) leftchildIndex(index int) int {
    return 2*index + 1
}

func (m *minheap) rightchildIndex(index int) int {
    return 2*index + 2
}

func (m *minheap) swap(first, second int) {
    temp := m.arr[first]
    m.arr[first] = m.arr[second]
    m.arr[second] = temp
}

func (m *minheap) leaf(index int, size int) bool {
    if index >= (size/2) && index <= size {
        return true
    }
    return false
}

func (m *minheap) downHeapify(current int, size int) {
    if m.leaf(current, size) {
        return
    }
    smallest := current
    leftChildIndex := m.leftchildIndex(current)
    rightRightIndex := m.rightchildIndex(current)
    if leftChildIndex < size && m.arr[leftChildIndex] < m.arr[smallest] {
        smallest = leftChildIndex
    }
    if rightRightIndex < size && m.arr[rightRightIndex] < m.arr[smallest] {
        smallest = rightRightIndex
    }
    if smallest != current {
        m.swap(current, smallest)
        m.downHeapify(smallest, size)
    }
    return
}

func (m *minheap) buildMinHeap(size int) {
    for index := ((size / 2) - 1); index >= 0; index-- {
        m.downHeapify(index, size)
    }
}

func (m *minheap) sort(size int) {
    m.buildMinHeap(size)
    for i := size - 1; i > 0; i-- {
        // Move current root to end
        m.swap(0, i)
        m.downHeapify(0, i)
    }
}

func (m *minheap) print() {
    for _, val := range m.arr {
        fmt.Println(val)
    }
}

func main() {
    inputArray := []int{6, 5, 3, 7, 2, 8, -1}
    minHeap := newMinHeap(inputArray)
    minHeap.sort(len(inputArray))
    minHeap.print()
    fmt.Scanln()
}

输出:

8
7
6
5
3
2
-1

时间复杂度

堆排序的时间复杂度为 O(nLogn)。

Go 语言中的二叉树高度或最大深度

来源:golangbyexample.com/height-binary-tree-golang/

目录

  • 概述

  • 程序

概述

目标是获取二叉树的高度。例如,假设我们有下面的二叉树

![](https://gitee.com/OpenDocCN/geekdoc-golang-zh/raw/master/docs/go-exam-2023/img/9a9347838908483552b24df3dc54cd38.png)

那么这里二叉树的高度是 3。

程序

下面是相同程序的代码

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func maxDepth(root *TreeNode) int {
	if root == nil {
		return 0
	}
	if root.Left == nil && root.Right == nil {
		return 1
	}

	lHeight := maxDepth(root.Left)
	rHeight := maxDepth(root.Right)

	if lHeight >= rHeight {
		return lHeight + 1
	} else {
		return rHeight + 1
	}
}

func main() {
	root := TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	height := maxDepth(&root)
	fmt.Println(height)

}

输出

3

注意: 请查看我们的 Golang 高级教程。本系列教程详尽,我们尝试涵盖所有概念及示例。本教程适合希望获得专业知识和对 golang 有扎实理解的读者 – Golang 高级教程

如果你对了解如何在 Golang 中实现所有设计模式感兴趣。如果是的话,这篇文章适合你 –所有设计模式 Golang

Go 中的 Hello World(Golang)

来源:golangbyexample.com/hello-world-golang/

让我们看看如何用 golang 编写一个简单的 Hello World 程序。创建一个扩展名为.go 的文件。我们将这个文件命名为helloworld.go。下面是文件的内容。

package main  

import "fmt" 

func main() { 
  fmt.Println("Hello World") 
}

关于上述程序需要注意的几点

  • 每个 go 文件都以包名开始。在上述情况下,它是包main

  • 只有main包是可执行的。

  • main包将包含一个main函数,标志着程序的开始

  • 我们使用fmt包的Println函数来打印Hello World字符串

fmt.Println("Hello World")

现在让我们运行这个文件。要运行,请转到包含此文件的目录。输入下面的命令来运行该文件。这个命令的作用是编译 go 文件并立即运行它。

go run helloworld.go

输出

Hello World

Golang 中的十六进制和八进制

来源:golangbyexample.com/hex-and-octal-in-golang/

目录

  • 概述

  • 十六进制数字

  • 八进制数字

  • 八进制与十六进制的组合示例

概述

  • 十六进制数字是基数 16。

  • 八进制数字是基数 8。

十六进制数字

十六进制数字可以在 Go 中使用前缀 0x 或 0X 表示。当 Go 看到任何以 0x 或 0X 开头的数字时,它将其视为十六进制数字。在下面的例子中,我们将0x14以十六进制添加到数字 10。0x14的十六进制等于20的十进制。因此输出将是 30。请参见转换表 – ascii.cl/conversion.htm

package main

import "fmt"

func main() {
    hexa20 := 0x14 //Equivalent to 20 in decimal
    output := 10 + hexa20
    fmt.Println(output)
}

输出:

30

八进制数字

八进制数字可以在 Go 中使用前缀0表示。当 Go 看到任何以0开头的数字时,它将其视为八进制数字。在下面的例子中,我们将024八进制添加到 10。024的八进制等于20的十进制。因此输出将是 30。

package main

import "fmt"

func main() {
    octa20 := 024 //Equivalent to 20 in decimal
    output := 10 + octa20
    fmt.Println(output)
}

输出:

30

八进制与十六进制的组合示例

在下面的例子中,0x14十六进制024八进制10相加。0x14十六进制等于20十进制024八进制等于20十进制。因此输出是 50。

package main

import "fmt"

func main() {
    hexa20 := 0x14 //Equivalent to 20 in decimal

    octa20 := 024 //Equivalent to 20 in decimal

    output := 10 + hexa20 + octa20
    fmt.Println(output)
}

输出:

50

Go(Golang)中的高阶函数

来源:golangbyexample.com/gohigher-order-functions/

目录

  • 概述

  • 代码:

    • 示例 1

    • 示例 2:

概述

高阶函数是指那些接受一个函数作为参数或返回一个函数的函数。由于在 Golang 中函数是一个一阶变量,它们可以被传递、返回,并赋值给一个变量。

代码:

在下面的示例 1 中

  • print 函数接受一个类型为 func(int, int) int 的函数作为参数

  • getAreafunc 返回一个类型为 func(int, int) int 的函数

示例 1

package main

import "fmt"

func main() {
    areaF := getAreaFunc()
    print(3, 4, areaF)
}

func print(x, y int, area func(int, int) int) {
    fmt.Printf("Area is: %d\n", area(x, y))
}

func getAreaFunc() func(int, int) int {
    return func(x, y int) int {
        return x * y
    }
}

输出:

12

示例 2:

我们再来看一个稍微复杂的例子,其中

  • 两个函数作为参数传递

  • 一个函数返回两个函数

package main

import "fmt"

func main() {
    add, subtract := getAddSubtract()
    print(3, 4, add, subtract)
}

func print(x, y int, add func(int, int) int, subtract func(int, int) int) {
    fmt.Printf("Sum is: %d\n", add(x, y))
    fmt.Printf("Difference Value is: %d\n", subtract(x, y))
}

func getAddSubtract() (func(int, int) int, func(int, int) int) {
    add := func(x, y int) int {
        return x + y
    }
    subtract := func(x, y int) int {
        return x - y
    }
    return add, subtract
}

输出

Sum is: 7
Difference Value is: 1
```*


<!--yml

类别:未分类

日期:2024-10-13 06:27:03

-->

# defer 在 Go(Golang)中是如何工作的

> 来源:[`golangbyexample.com/how-defer-works-golang/`](https://golangbyexample.com/how-defer-works-golang/)

目录

+   概述

+   单个 Defer

+   特定函数中的多个 Defer 函数

+   不同函数中的多个 Defer 函数

# **概述**

当编译器在一个函数中遇到 defer 语句时,它会将其推入一个列表。该列表内部实现了类似堆栈的数据结构。同一函数中遇到的所有 defer 语句都会被推入这个列表。当外部函数返回时,堆栈中的所有函数将从上到下依次执行,然后才能开始调用函数的执行。调用函数中也会发生相同的事情。

让我们看三个 defer 的例子,更好地理解 defer 在 Go 中的工作原理。

+   单个 defer

+   同一函数中的多个 defer

+   不同函数中的不同 defer

让我们看每个示例

# **单个 Defer**

```go
package main
import "fmt"
func main() {
    defer test()
    fmt.Println("Executed in main")
}
func test() {
    fmt.Println("In Defer")
}

输出

Executed in main
In Defer

在上面的程序中,有一个defer语句调用了名为test的自定义函数。从输出可以看出,test函数在主函数的所有内容执行完后被调用,并在主函数返回之前执行。这就是原因。

Executed in main

在此之前会打印

In Defer

特定函数中的多个 Defer 函数

如果在特定函数中有多个 defer 函数,则所有 defer 函数将按照后进先出的顺序执行,这与我们上述提到的相似。

让我们看一下这个程序。

package main
import "fmt"
func main() {
    i := 0
    i = 1
    defer fmt.Println(i)
    i = 2
    defer fmt.Println(i)
    i = 3
    defer fmt.Println(i)
}

输出

3
2
1

在上面的程序中,我们有三个defer函数,每个函数打印i的值。变量i在每个 defer 之前递增。代码首先输出 3,表示第三个 defer 函数首先执行。然后输出 2,表示第二个 defer 在其后执行,最后输出 1,表示第一个 defer 最后执行。这表明,当特定函数中有多个 defer 函数时,遵循“后进先出”的规则。这就是程序输出的原因。

3
2
1

不同函数中的多个 Defer 函数

让我们理解当在不同函数中有多个 defer 函数时会发生什么。想象一下从main函数调用f1函数,再调用f2函数的情况。

main->f1->f2

以下是 f2 返回后将发生的顺序

  • 如果存在f2中的 defer 函数,将执行这些 defer 函数。控制将返回给调用者,即函数f1

  • 如果在f1中存在 defer 函数,它们将被执行。控制将返回给调用者,即函数main。请注意,如果中间有更多函数,处理将以类似的方式向上继续执行。

  • 在主函数返回后,如果主函数中存在 defer 函数,它将被执行。

让我们来看一个程序示例。

package main

import "fmt"

func main() {
	defer fmt.Println("Defer in main")
	fmt.Println("Stat main")
	f1()
	fmt.Println("Finish main")
}

func f1() {
	defer fmt.Println("Defer in f1")
	fmt.Println("Start f1")
	f2()
	fmt.Println("Finish f1")
}

func f2() {
	defer fmt.Println("Defer in f2")
	fmt.Println("Start f2")
	fmt.Println("Finish f2")
}

输出

Stat main
Start f1
Start f2
Finish f2
Defer in f2
Finish f1
Defer in f1
Finish main
Defer in main

如何在 Go(Golang)中从另一个包访问结构体

来源:golangbyexample.com/struct-another-package-golang/

目录

  • 概述

  • 程序

概述

另一个包中的结构体名称必须以大写字母开头,这样它才能在包外公开。如果结构体名称以小写字母开头,则在包外不可见。

要在包外访问一个结构体,我们需要先导入包含该结构体的包。

程序

这是相应的代码

go.mod

module sample.com/learn
go 1.16

model/person.go

package model
type Person struct {
    Name string
}

main.go

package main
import (
    "fmt"
    "sample.com/learn/person"
)
func main() {
    p := &model.Person{
        Name: "John",
    }
    fmt.Println(p.Name)
}

在这个程序中,我们首先从main包导入model包,如下所示。

"sample.com/learn/model"

然后我们从main包访问Person结构体如下。

p := &model.Person{
   Name: "John",
}

之所以可行,是因为Person结构体名称是大写的。

让我们将结构体名称改为小写并运行这个程序。它会产生以下编译错误

cannot refer to unexported name model.person

另请查看我们的 Golang 综合教程系列 – Golang 综合教程

如何在 Go (Golang)中从另一个包调用函数

来源:golangbyexample.com/functoin-different-package-go/

目录

  • 概述

  • 程序

概述

另一个包中的函数必须以大写字母开头,以便在其包外部是公共的。如果函数名以小写字母开头,则在其包外部将不可见。

要在包外使用函数,我们需要先导入包含该函数的包。

程序

这是相同的代码。

go.mod

module sample.com/learn
go 1.16

hello/hello.go

package hello

import "fmt"

func SayHello() {
	fmt.Println("Hello")
}

main.go

package main

import "sample.com/learn/hello"

func main() {
    hello.SayHello()
}

输出

Hello

在这个程序中,我们首先从main包导入hello包,如下所示。

import "sample.com/learn/hello"

然后我们从main包调用SayHello函数,如下所示。

hello.SayHello()

之所以有效,是因为SayHello函数是大写的。

将函数改为小写并运行此程序。它会产生以下编译错误。

cannot refer to unexported name hello.sayHello

还可以查看我们的 Golang 综合教程系列 – Golang 综合教程

如何检查 Go (Golang) 中的映射是否包含某个键

来源:golangbyexample.com/check-map-key-golang/

以下是检查映射中是否存在键的格式。

val, ok := mapName[key]

有两种情况

  • 如果键存在,val 变量将是映射中该键的值,而 ok 变量将为真。

  • 如果键不存在,val 变量将是值类型的默认零值,而 ok 变量将为假。

让我们来看一个例子

package main

import "fmt"

func main() {
    //Declare
    employeeSalary := make(map[string]int)

    //Adding a key value
    employeeSalary["Tom"] = 2000
    fmt.Println("Key exists case")
    val, ok := employeeSalary["Tom"]
    fmt.Printf("Val: %d, ok: %t\n", val, ok)
    fmt.Println("Key doesn't exists case")

    val, ok = employeeSalary["Sam"]
    fmt.Printf("Val: %d, ok: %t\n", val, ok)
}

输出

Key exists case
Val: 2000, ok: true
Key doesn't exists case
Val: 0, ok: false

在上面的程序中,当键存在时,val 变量被设置为实际值,这里是 2000,ok 变量为真。当键不存在时,val 变量被设置为 0,这也是 int 的默认零值,ok 变量为假。这个 ok 变量是检查键是否存在于映射中的最佳方式。

如果我们只想检查一个键是否存在,而不需要 val,则可以用空标识符“_”代替 val。

_, ok = employeeSalary["Sam"]

另外,请查看我们的 Golang 进阶教程系列 – Golang 进阶教程

posted @ 2024-10-19 08:38  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报