0%

在x分钟内学会go

参考资料:


go 类型

类型分类 类型示例 定义语法
基本类型 int, float32, bool, string, rune var x int var y float32 var isTrue bool var s string
复合类型 array, slice, map, struct var arr [5]int var s []int var m map[string]int type Person struct { Name string; Age int }
引用类型 slice, map, channel, pointer var p *int var ch chan int var s []int var m map[string]int
接口类型 interface{}, 自定义接口 var i interface{} type Animal interface { Speak() string }
函数类型 函数类型可以作为类型,传递函数 type AddFunc func(a int, b int) int var sum AddFunc
常量类型 常量类型通过 const 声明,值不可更改 const Pi = 3.14 const Name = "Go"
类型别名 type NewType oldType 用于为类型创建别名 type MyInt int var x MyInt

代码通识💡


// 单行注释
/* 多行注释 */

/* 构建标签是以 //go:build 开头的行注释
可以通过 go build -tags="foo bar" 命令执行。
构建标签放在包声明之前,靠近文件顶部
后面跟着一个空行或其他行注释。 */
//go:build prod || dev || test

// 每个源文件都以包声明开始。
// main 是一个特殊名称,声明一个可执行文件而不是库。
package main

// 导入声明声明在此文件中引用的库包。
import (
"fmt" // Go 标准库中的一个包。
"io" // 实现一些 I/O 工具函数。
m "math" // 带有本地别名 m 的数学库。
"net/http" // 是的,一个网络服务器!
_ "net/http/pprof" // 仅为副作用导入的分析库
"os" // 像处理文件系统的 OS 函数
"strconv" // 字符串转换。
)

// 函数定义。Main 是特殊的。它是可执行程序的入口点。
// 喜欢它或讨厌它,Go 使用大括号。
func main() {
// Println 输出一行到标准输出。
// 它来自 fmt 包。
fmt.Println("Hello world!")

// 在此包内调用另一个函数。
beyondHello()
}

// 函数可以有参数在括号中。
// 如果没有参数,空括号仍然是必需的。
func beyondHello() {
var x int // 变量声明。变量必须在使用前声明。
x = 3 // 变量赋值。
// "短"声明使用 := 来推断类型,声明并赋值。
y := 4
sum, prod := learnMultiple(x, y) // 函数返回两个值。
fmt.Println("sum:", sum, "prod:", prod) // 简单输出。
learnTypes() // < y 分钟,了解更多!
}

/* <- 多行注释
函数可以有参数和(多个!)返回值。
这里 `x`,`y` 是参数,`sum`,`prod` 是签名(返回的内容)。
注意 `x` 和 `sum` 接收类型 `int`。
*/
func learnMultiple(x, y int) (sum, prod int) {
return x + y, x * y // 返回两个值。
}

// 一些内置类型和字面量。
func learnTypes() {
// 短声明通常会给你想要的东西。
str := "Learn Go!" // 字符串类型。

s2 := `A "raw" string literal
can include line breaks.` // 同样的字符串类型。

// 非 ASCII 字面量。Go 源代码是 UTF-8。
g := 'Σ' // rune 类型,是 int32 的别名,保存一个 unicode 码点。

f := 3.14159 // float64,IEEE-754 64 位浮点数。
c := 3 + 4i // complex128,内部用两个 float64 表示。

// 带有初始化器的 var 语法。
var u uint = 7 // 无符号,但实现依赖于大小,与 int 一样。
var pi float32 = 22. / 7

// 转换语法与短声明。
n := byte('\n') // byte 是 uint8 的别名。

// 数组的大小在编译时固定。
var a4 [4]int // 一个包含 4 个 int 的数组,初始化为全 0。
a5 := [...]int{3, 1, 5, 10, 100} // 一个初始化为固定大小为五的数组
// 元素,值为 3,1,5,10 和 100。

// 数组具有值语义。
a4_cpy := a4 // a4_cpy 是 a4 的副本,两个独立的实例。
a4_cpy[0] = 25 // 只有 a4_cpy 被更改,a4 保持不变。
fmt.Println(a4_cpy[0] == a4[0]) // false

// 切片具有动态大小。数组和切片各有优缺点
// 但切片的使用场景更为常见。
s3 := []int{4, 5, 9} // 与 a5 相比。这里没有省略号。
s4 := make([]int, 4) // 分配 4 个 int 的切片,初始化为全 0。
var d2 [][]float64 // 仅声明,没有分配任何内容。
bs := []byte("a slice") // 类型转换语法。

// 切片(以及映射和通道)具有引用语义。
s3_cpy := s3 // 两个变量指向同一个实例。
s3_cpy[0] = 0 // 这意味着两个都被更新。
fmt.Println(s3_cpy[0] == s3[0]) // true

// 因为它们是动态的,切片可以按需追加。
// 要向切片追加元素,使用内置的 append() 函数。
// 第一个参数是我们要追加的切片。通常,
// 切片变量就地更新,如下面的示例。
s := []int{1, 2, 3} // 结果是一个长度为 3 的切片。
s = append(s, 4, 5, 6) // 添加了 3 个元素。切片现在的长度为 6。
fmt.Println(s) // 更新后的切片现在是 [1 2 3 4 5 6]

// 要追加另一个切片,而不是原子元素的列表,我们可以
// 传递对切片的引用或像这样带有尾随省略号的切片字面量,
// 意思是取一个切片并展开其元素,
// 将它们追加到切片 s。
s = append(s, []int{7, 8, 9}...) // 第二个参数是一个切片字面量。
fmt.Println(s) // 更新后的切片现在是 [1 2 3 4 5 6 7 8 9]

p, q := learnMemory() // 声明 p,q 为指向 int 的指针。
fmt.Println(*p, *q) // * 后跟指针。这将打印两个 int。

// 映射是动态可增长的关联数组类型,类似于某些其他语言的
// 哈希或字典类型。
m := map[string]int{"three": 3, "four": 4}
m["one"] = 1
// 检查键是否存在于映射中,如下所示:
if val, ok := m["one"]; ok {
// 做一些事情
}

// 未使用的变量在 Go 中是一个错误。
// 下划线让你"使用"一个变量但丢弃其值。
_, _, _, _, _, _, _, _, _, _ = str, s2, g, f, u, pi, n, a5, s4, bs
// 通常你用它来忽略函数的返回值之一
// 例如,在一个快速而肮脏的脚本中,你可能会忽略
// 从 os.Create 返回的错误值,并期望文件
// 总是会被创建。
file, _ := os.Create("output.txt")
fmt.Fprint(file, "这就是你如何写入文件,顺便说一下")
file.Close()

// 当然,输出也算作使用一个变量。
fmt.Println(s, c, a4, s3, d2, m)

learnFlowControl() // 回到流程控制。
}

// 在许多其他语言中,Go 中的函数
// 可以有命名返回值。
// 在函数声明行中为返回的类型分配一个名称
// 允许我们轻松地从函数中的多个点返回,以及
// 只使用 return 关键字,而不需要其他内容。
func learnNamedReturns(x, y int) (z int) {
z = x * y
return // z 在这里是隐式的,因为我们之前命名了它。
}

// Go 是完全垃圾回收的。它有指针但没有指针运算。
// 你可以犯一个 nil 指针的错误,但不会通过递增指针。
// 与 C/C++ 不同,获取和返回局部变量的地址也是安全的。
func learnMemory() (p, q *int) {
// 命名返回值 p 和 q 的类型是指向 int 的指针。
p = new(int) // 内置函数 new 分配内存。
// 分配的 int 切片初始化为 0,p 不再是 nil。
s := make([]int, 20) // 分配 20 个 int 作为一块内存。
s[3] = 7 // 赋值其中一个。
r := -2 // 声明另一个局部变量。
return &s[3], &r // & 获取对象的地址。
}

// 使用别名数学库(见上面的导入)
func expensiveComputation() float64 {
return m.Exp(10)
}

func learnFlowControl() {
// 如果语句需要大括号,并且不需要括号。
if true {
fmt.Println("我告诉过你")
}
// 格式化由命令行命令 "go fmt" 标准化。
if false {
// 呜呜。
} else {
// 得意。
}
// 使用 switch 优先于链式 if 语句。
x := 42.0
switch x {
case 0:
case 1, 2: // 可以在一个 case 中有多个匹配
case 42:
// case 不会"穿透"。
/*
然而,有一个 `fallthrough` 关键字,见:
https://go.dev/wiki/Switch#fall-through
*/
case 43:
// 未到达。
default:
// 默认情况是可选的。
}

// 类型开关允许根据某个东西的类型而不是值进行切换
var data interface{}
data = ""
switch c := data.(type) {
case string:
fmt.Println(c, "是一个字符串")
case int64:
fmt.Printf("%d 是一个 int64\n", c)
default:
// 所有其他情况
}

// 与 if 一样,for 也不使用括号。
// 在 for 和 if 中声明的变量是局部的。
for x := 0; x < 3; x++ { // ++ 是一个语句。
fmt.Println("迭代", x)
}
// x == 42 在这里。

// for 是 Go 中唯一的循环语句,但它有替代形式。
for { // 无限循环。
break // 开玩笑的。
continue // 未到达。
}

// 你可以使用 range 遍历数组、切片、字符串、映射或通道。
// range 返回一个(通道)或两个值(数组、切片、字符串和映射)。
for key, value := range map[string]int{"one": 1, "two": 2, "three": 3} {
// 对于映射中的每一对,打印键和值
fmt.Printf("key=%s, value=%d\n", key, value)
}
// 如果你只需要值,使用下划线作为键
for _, name := range []string{"Bob", "Bill", "Joe"} {
fmt.Printf("你好,%s\n", name)
}

// 与 for 一样,:= 在 if 语句中意味着先声明并赋值
// y,然后测试 y > x。
if y := expensiveComputation(); y > x {
x = y
}
// 函数字面量是闭包。
xBig := func() bool {
return x > 10000 // 引用上面 switch 语句中声明的 x。
}
x = 99999
fmt.Println("xBig:", xBig()) // true
x = 1.3e3 // 这使得 x == 1300
fmt.Println("xBig:", xBig()) // 现在是 false。

// 更重要的是,函数字面量可以在线定义和调用,
// 作为函数的参数,只要:
// a) 函数字面量立即调用(),
// b) 结果类型与参数的预期类型匹配。
fmt.Println("加 + 乘以两个数字:",
func(a, b int) int {
return (a + b) * 2
}(10, 2)) // 用参数 10 和 2 调用
// => 加 + 乘以两个数字:24

// 当你需要它时,你会喜欢它。
goto love
love:

learnFunctionFactory() // func 返回 func 是有趣的(3)(3)
learnDefer() // 一个快速的绕道到一个重要的关键字。
learnInterfaces() // 好东西即将到来!
}

func learnFunctionFactory() {
// 接下来的两个是等效的,第二个更实用
fmt.Println(sentenceFactory("summer")("一个美丽的", "日子!"))

d := sentenceFactory("summer")
fmt.Println(d("一个美丽的", "日子!"))
fmt.Println(d("一个懒惰的", "下午!"))
}

// 装饰器在其他语言中很常见。Go 中也可以做到
// 使用接受参数的函数字面量。
func sentenceFactory(mystring string) func(before, after string) string {
return func(before, after string) string {
return fmt.Sprintf("%s %s %s", before, mystring, after) // 新字符串
}
}

//defer-后进先出(LIFO)顺序执行
func learnDefer() (ok bool) {
// defer 语句将函数调用推送到列表中。保存的
// 调用列表在周围的函数返回后执行。
defer fmt.Println("延迟语句以相反的顺序执行(LIFO)。")
defer fmt.Println("\n这一行首先被打印,因为")
// defer 通常用于关闭文件,因此关闭文件的函数
// 保持在打开文件的函数附近。
return true
}

// 定义 Stringer 作为具有一个方法 String 的接口类型。
type Stringer interface {
String() string
}

// 定义 pair 作为具有两个字段的结构体,命名为 x 和 y 的 int。
type pair struct {
x, y int
}

// 在类型 pair 上定义一个方法。Pair 现在实现了 Stringer,因为 Pair 定义了接口中的所有方法。
func (p pair) String() string { // p 被称为"接收者"
// Sprintf 是 fmt 包中的另一个公共函数。
// 点语法引用 p 的字段。
return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

func learnInterfaces() {
// 大括号语法是"结构字面量"。它计算为初始化
// 结构。:= 语法声明并初始化 p 为此结构。
p := pair{3, 4}
fmt.Println(p.String()) // 调用类型为 pair 的 p 的 String 方法。
var i Stringer // 声明 i 为接口类型 Stringer。
i = p // 有效,因为 pair 实现了 Stringer
// 调用类型为 Stringer 的 i 的 String 方法。输出与上面相同。
fmt.Println(i.String())

// fmt 包中的函数调用 String 方法以请求对象
// 的可打印表示。
fmt.Println(p) // 输出与上面相同。Println 调用 String 方法。
fmt.Println(i) // 输出与上面相同。

learnVariadicParams("很棒的", "学习", "在这里!")
}

// 函数可以有变参。
func learnVariadicParams(myStrings ...any) { // any 是 interface{} 的别名
// 迭代每个变参的值。
// 这里的下划线忽略了数组的索引参数。
for _, param := range myStrings {
fmt.Println("参数:", param)
}

// 将变参值作为变参传递。
fmt.Println("参数:", fmt.Sprintln(myStrings...))

learnErrorHandling()
}

func learnErrorHandling() {
// ", ok" 习语用于告诉某件事情是否成功。
m := map[int]string{3: "three", 4: "four"}
if x, ok := m[1]; !ok { // ok 将为 false,因为 1 不在映射中。
fmt.Println("没有人")
} else {
fmt.Print(x) // 如果 x 在映射中,它将是值。
}
// 错误值不仅传达"ok",还传达更多关于问题的信息。
if _, err := strconv.Atoi("非整数"); err != nil { // _ 丢弃值
// 打印 'strconv.ParseInt: parsing "non-int": invalid syntax'
fmt.Println(err)
}
// 我们稍后会重新访问接口。与此同时,
learnConcurrency()
}

// c 是一个通道,一个并发安全的通信对象。
func inc(i int, c chan int) {
c <- i + 1 // <- 是当通道出现在左侧时的"发送"运算符。
}

// 我们将使用 inc 来并发地递增一些数字。
func learnConcurrency() {
// 与之前用于创建切片的 make 函数相同。Make 分配和
// 初始化切片、映射和通道。
c := make(chan int)
// 启动三个并发的 goroutine。数字将被递增
// 并发地,可能在机器能够并且
// 正确配置的情况下并行。所有三个发送到同一个通道。
go inc(0, c) // go 是一个语句,启动一个新的 goroutine。
go inc(10, c)
go inc(-805, c)
// 从通道读取三个结果并打印它们。
// 没有办法知道结果将以什么顺序到达!
fmt.Println(<-c, <-c, <-c) // 通道在右侧,<- 是"接收"运算符。

cs := make(chan string) // 另一个通道,这个处理字符串。
ccs := make(chan chan string) // 一个字符串通道的通道。
go func() { c <- 84 }() // 启动一个新的 goroutine 仅仅发送一个值。
go func() { cs <- "wordy" }() // 再次,为 cs 这次。
// select 的语法类似于 switch 语句,但每个 case 涉及
// 通道操作。它随机选择准备通信的 case。
select {
case i := <-c: // 接收到的值可以分配给一个变量,
fmt.Printf("它是一个 %T", i)
case <-cs: // 或者接收到的值可以被丢弃。
fmt.Println("它是一个字符串")
case <-ccs: // 空通道,未准备好通信。
fmt.Println("没有发生。")
}
// 此时,从 c 或 cs 中取出了一个值。上面启动的两个
// goroutine 中的一个已经完成,另一个将保持阻塞。

learnWebProgramming() // Go 做到了。你也想这样做。
}

// package http 中的一个单一函数启动一个网络服务器。
func learnWebProgramming() {
// ListenAndServe 的第一个参数是要监听的 TCP 地址。
// 第二个参数是一个接口,特别是 http.Handler。
go func() {
err := http.ListenAndServe(":8080", pair{})
fmt.Println(err) // 不要忽略错误
}()

requestServer()
}

// 通过实现其唯一方法 ServeHTTP,使 pair 成为 http.Handler。
func (p pair) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 使用 http.ResponseWriter 的方法提供数据。
w.Write([]byte("你在 Y 分钟内学会了 Go!"))
}

func requestServer(){
//函数向 http://localhost:8080 发送一个 GET 请求。
resp,err := http.Get("http://localhost:8080")
fmt.Println(err)
defer resp.Body.Close()//在函数执行完毕后关闭请求
//读取 resp.Body 中的所有内容-响应体
body,err := io.ReadAll(resp.Body)
fmt.Printf("\nWebserver 说: `%s`", string(body))
}

难点解析💡

一、装饰器

装饰器的概念:

  • 装饰器(Decorator) 是一种常见的设计模式,目的是在不改变原有函数的情况下对其功能进行扩展或修改。在 Go 语言中,通过闭包可以实现类似装饰器的功能。
  • 在本例中,sentenceFactory 就是一个简单的装饰器,它将一个常量字符串(mystring)和传入的参数组合成新的句子。
  1. sentenceFactory 函数工厂:

    sentenceFactory 是一个函数工厂,它接收一个字符串(如 "summer")并返回一个新的函数,这个返回的函数接收两个参数 beforeafter,并返回一个格式化的字符串。

    func sentenceFactory(mystring string) func(before, after string) string {
    return func(before, after string) string {
    return fmt.Sprintf("%s %s %s", before, mystring, after)
    }
    }

    这个函数工厂的核心是:

    • 它返回一个匿名函数(也叫做闭包),这个匿名函数接受两个字符串参数 beforeafter,并通过 fmt.Sprintf 格式化并返回一个新的字符串。
    • 在格式化的过程中,mystring 是函数工厂的输入参数,代表固定的一部分字符串。beforeafter 则是调用这个函数时传入的参数。
  2. learnFunctionFactory 使用:

    learnFunctionFactory 函数演示了如何使用 sentenceFactory

    func learnFunctionFactory() {
    fmt.Println(sentenceFactory("summer")("一个美丽的", "日子!"))

    d := sentenceFactory("summer")
    fmt.Println(d("一个美丽的", "日子!"))
    fmt.Println(d("一个懒惰的", "下午!"))
    }
    • sentenceFactory("summer")("一个美丽的", "日子!") 这一行创建了一个使用 "summer" 字符串的函数,并立即调用它,传入 "一个美丽的""日子!" 作为参数。输出将是:"一个美丽的 summer 日子!".
    • d := sentenceFactory("summer") 先将工厂返回的函数赋值给 d,然后可以多次调用 d,传入不同的参数,生成不同的句子。第二次调用时传入的是 "一个懒惰的""下午!",输出将是:"一个懒惰的 summer 下午!"