likes
comments
collection
share

后端语言很难?前端入门go基础语法只需要3小时!(中)

作者站长头像
站长
· 阅读数 16

继续接着第一部分,上面我们讲到了复合数据类型的数组,现在我们接着讲跟数组息息相关的切片,切片其实跟js的数组概念很像

第一部分:后端语言很难?前端入门go基础语法只需要3小时!(上)

顺便给大家看下,node和go的性能对比

后端语言很难?前端入门go基础语法只需要3小时!(中)

就上图的三个指标来说的话,前端的话,express不堪一击。。。(node中最快的前端框架是fastify,比上图的koa快一些,但不是很多)

想要go资料的直接私信我,我发给你,包括书和视频,一起go!

切片

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

后端语言很难?前端入门go基础语法只需要3小时!(中)

我们讲讲指针,长度和容量。

在 Go 语言中,一个 slice 是由以下三部分组成的:

  • 指针: 指向底层数组中第一个元素的地址
  • 长度: slice 中元素的数量
  • 容量: 从第一个元素开始,slice 能够访问的元素总数

这三部分共同组成了一个 slice。指针和长度组成了 slice 的视图,指向底层数组的一段连续的元素,而容量则是指 slice 最多能够访问的元素数。

这个长度和容量是可以通过len(s)cap(s) 函数来获徖的。

例如:

arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3]
fmt.Println("length:", len(s))
fmt.Println("capacity:", cap(s))

第一行,创建了一个底层数组,第二行创建了一个 slice s, 包含了底层数组中的第二个和第三个元素,所以s的长度为2,容量为4,因为s 可以访问到底层数组的第二个和第三个元素,还可以访问第四个和第五个元素。

类比js,非常需要注意的是,slice居然可以共享底层的数据,在js里是不存在基础值还能共享的

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义

months := [...]string{1: "January", /* ... */, 12: "December"}

因此一月份是months[1],十二月份是months[12]。通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始化为空字符串。

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice,它们有重叠部分:

后端语言很难?前端入门go基础语法只需要3小时!(中)

Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2)     // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]

这意味着,其中一个切片改变数据,另一个切片也会受到影响.举个例子

package main

import (
	"fmt"
)

func main() {
	month := []string{"1", "2", "3"}
	s1 := month[0:2]
	s2 := month[1:3]
	s1[1] = "1"
	fmt.Println(s1, s2) // [1 1] [1 3]
}

要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。

s := []int{0, 1, 2, 3, 4, 5}

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    for i := range x {
        if x[i] != y[i] {
            return false
        }
    }
    return true
}

我们讲解一下为啥slice不能直接比较:

在 Go 语言中,slice 是对数组的引用,因此它们包含指向底层数组的指针、长度和容量三个部分。在进行比较时,这三个部分都必须完全相同才能视为相等,而在实际应用中,这很难做到。因此在 Go 语言中,slice 是不能直接进行比较的

而且因为是引用,值随时会变,这就有点搞了。。。

slice唯一合法的比较操作是和nil比较

if summer == nil { /* ... */ }

一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的。如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len)

make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

这里我们顺带介绍一下make语法:

make

make 是 Go 语言中的一个内建函数,它主要用来创建并初始化内置的数据类型。它可以用来创建:

  • slice
  • map
  • channel

语法格式如下:

make(type, size, capacity)
  • type 指定要创建的数据类型,可以是 slicemapchannel
  • size 指定创建的数据类型的长度。对于 slicemap,它指定了初始元素的数量;对于 channel,它指定了缓存大小。
  • capacity 主要用于创建 slice,它指定了底层数组的大小,即最多可以容纳的元素数量。

示例:

// create a slice
s := make([]int, 5, 10)
fmt.Println(s) // [0 0 0 0 0]

// create a map
m := make(map[string]int, 100)
fmt.Println(m) // map[]

// create a channel
c := make(chan int, 10)
fmt.Println(c) //0xc420012010

注意:

  • 使用 make 创建的 map 和 slice 都是可以直接使用的,不需要再次初始化。
  • make 创建的 map,默认都是 nil, 所以不能直接对其进行操作, 需要先插入元素。
  • 在使用 make 创建 slice 的时候,如果省略 capacity 参数,则默认与 size 相同。

对比js,js创建数组和普通对象比go方便很多,毕竟有数组和对象字面量

Map

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法。

内置的make函数可以创建一个map:

ages := make(map[string]int) // mapping from strings to ints

我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:

ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

这相当于

ages := make(map[string]int)

ages["alice"] = 31

ages["charlie"] = 34

使用内置的delete函数可以删除元素:

delete(ages, "alice") // remove element ages["alice"]

类比js:这个跟js差距还是很大的,跟ts也是,我们知道js可以定义一个map,然后map的key和value基本上任意类型都可以,而且可以定义key为引用类型,布尔值,string都行,而且不必都是相同的类型,go则强制必须是相同的类型,因为go的类型系统不像ts有联合类型

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:

import "sort"

var names []string
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}

类比js这个跟js里的map遍历,显示的key不同,js有一套自己的顺序,比如数字字符串比字母的靠前,反正不是根据添加到对象的顺序遍历的,比如说:

let o = { age: 14, 1: 'zhangsan' }
Object.keys(o) //  ['1', 'age']

但js可以通过Map的数据结构保证遍历顺序。

继续go的内容。

因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:

names := make([]string, 0, len(ages))

map类型的零值是nil,也就是没有引用任何哈希表。

var ages map[string]int

fmt.Println(ages == nil) // "true"

fmt.Println(len(ages) == 0) // "true"

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:

ages["carol"] = 21 // panic: assignment to entry in nil map

在向map存数据前必须先创建map。

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值,正如我们前面看到的ages["bob"]那样。这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可以需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:

age, ok := ages["bob"]

if !ok { /* "bob" is not a key in this map; age == 0. */ }

在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

你会经常看到将这两个结合起来使用,像这样:

if age, ok := ages["bob"]; !ok { /* ... */ }

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:

func equal(x, y map[string]int) bool {
    if len(x) != len(y) {
        return false
    }
    for k, xv := range x {
        if yv, ok := y[k]; !ok || yv != xv {
            return false
        }
    }
    return true
}

Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。

类比js,我感觉es6后的js,数据类型结构丰富了非常多,从这个层面上讲,加上灵活性,我觉得是比go的数据结构更有优势的

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。

对比js,其实结构体跟我们js的对象的概念要更接近,因为值的类型可以是各种类型的聚合,这种类型对面向对象是更合适的,毕竟你创建的对象不一定都是一个类型

下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert:

type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee

dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Name和dilbert.DoB。因为dilbert是一个变量,它所有的成员也同样是变量,我们可以直接对每个成员赋值:

dilbert.Salary -= 5000 // demoted, for writing too few lines of code

或者是对成员取地址,然后通过指针访问:

position := &dilbert.Position

*position = "Senior " + *position // promoted, for outsourcing to Elbonia

点操作符也可以和指向结构体的指针一起工作:

var employeeOfTheMonth *Employee = &dilbert

employeeOfTheMonth.Position += " (proactive team player)"

我们再举个更简单的例子,介绍结构体:

在 Go 语言中,结构体(struct)是一种用于组合数据的类型。结构体是一组命名字段,每个字段都有一个类型。

一个结构体可以由以下语法定义:

type structName struct {
    field1 type1
    field2 type2
    field3 type3
    ...
}

例如:

type Person struct {
    Name string
    Age int
    Gender string
}

结构体字段可以通过.来访问, 比如:

p := Person{Name: "John", Age: 25, Gender: "male"}
fmt.Println(p.Name) // Output: John

JSON

类比js:json跟js里的差不多,因为json的规范是一个公共的协议,每个语言都要遵守,比如说json里的引用类型只能是数组和普通对象,比如函数是不行的,所以你可以了解一下js中的json语法,对你了解go的json语法大有裨益。

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和map等聚合数据类型。

基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性。

Go语言标准库中包含了一个名为 "encoding/json" 的包,用于处理 JSON 数据。它提供了编码和解码 JSON 数据的函数。

使用该包可以方便地将 Go 中的结构体编码为 JSON 字符串,也可以将 JSON 字符串解码为 Go 中的结构体。

编码:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string
    Age  int
}

func main() {
    user := &User{
        Name: "Bob",
        Age:  30,
    }

    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return
    }

    fmt.Println(string(jsonData))
}

如果你想让打印的json字符串有缩进,可以使用 MarshalIndent

data, err := json.MarshalIndent(user, "", " ")

类比js json.Marshal相当于我们的JSON.stringify,但是需要注意, json.Marshal传入的是一个对象指针,而不是对象,至于为什么这么做,是因为go里面的对象参数,都会复制一份传入的参数,为了节约空间和时间,一般都传的是指针。

解码:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string
    Age  int
}

func main() {
    jsonData := []byte(`{"Name":"Bob","Age":30}`) // 不理解byte类型无所谓,主要是看json的操作

    var user User
    err := json.Unmarshal(jsonData, &user)
    if err != nil {
        fmt.Println("Error decoding JSON:", err)
        return
    }

    fmt.Println(user)
}

通过上面的代码我们可以看到,json包提供了 Marshal 和 Unmarshal 两个函数来完成编码和解码操作。

在上面的例子中,Marshal 函数将 User 结构体编码为 JSON 数据, Unmarshal 函数将 JSON 数据解码为 User 结构体。

需要注意的是,如果你打算将 Go 中的结构体编码为 JSON,这些结构体的字段名必须是可导出的(即首字母大写)

类比js:相当于js的JSON.pase,但是go的json.Unmarshal第二个参数需要传一个指针,把第一个参数对象的值赋予给它,其实我们js内部也是这样实现的,只不过js内部帮我们自动传入的第二个参数,暴露的API让整个API看起来更简单

go标准库中template

Go 的标准库中的 "template" 包提供了用于生成文本输出的功能。它可以用来生成 HTML、XML、JSON 等格式的文本输出。

使用 "template" 包时,首先需要创建一个模板,其中可以包含文本和特殊的指令。指令以"{{}}" 来标识,用于插入变量的值或执行逻辑操作。

例如:

package main

import (
	"os"
	"text/template"
)

func main() {
	tmpl, err := template.New("test").Parse("Hello, {{.Name}}!")
	if err != nil {
		panic(err)
	}
	
	err = tmpl.Execute(os.Stdout, map[string]string{"Name": "Alice"})
	if err != nil {
		panic(err)
	}
}

在上面的例子中,使用了 "template.New" 和 "Parse" 函数创建了一个名为 "test" 的模板,并用 "Hello, {{.Name}}!" 作为模板内容。然后,使用 "Execute" 函数将模板和数据结构(这里是一个map)进行绑定,并将生成的文本输出到标准输出。

类比js: node中也有很多模板库,但都是第三方的,go属于内置的模板,js是有模板语法,但是js里的模板语法没有流程控制能力,比如写if else(当然可以用三元运算符模拟,因为三元运算是一个表达式,不是语句),写循环,所以go的模板语法更像是node里的模板库

控制流指令

模板包提供了一些控制流指令,例如 "if"、"range" 等,可以用来执行条件和循环操作。

{{if .IsAdmin}}
    <p>You are an admin</p>
{{else}}
    <p>You are not an admin</p>
{{end}}

{{range .Items}}
    <li>{{.}}</li>
{{end}}

模板函数

可以在模板中使用自定义函数来执行复杂的操作。需要先在 Go 代码中定义函数,然后将其注册到模板中。

func double(n int) int {
	return n * 2
}

tmpl := template.Must(template.New("test").Funcs(template.FuncMap{
	"double": double,
}).Parse("{{double .Value}}"))

data := map[string]int{"Value": 5}
tmpl.Execute(os.Stdout, data)

函数

Go语言中的函数是一种可以被命名并可以被重复调用的代码块。在 Go 中,函数是一等公民,可以被赋值给变量、作为参数传递给其他函数,也可以作为其他函数的返回值。

函数的定义格式如下:

func function_name(parameter_list) return_type {
    // function body
}

其中,function_name 是函数的名称,parameter_list 是参数列表,return_type 是返回值类型。

例如,下面是一个求两个整数和的函数:

func add(a int, b int) int {
    return a + b
}

在 Go 中,函数可以返回多个值。例如,下面的函数返回两个值:

func swap(a, b int) (int, int) {
    return b, a
}

函数也可以有可选的返回值名称,如下面的例子:

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

在 Go 中,函数可以接受一个或多个参数,也可以返回一个或多个值。参数和返回值都可以有类型,也可以省略类型。

Go 中还有一种特殊的函数类型,称为高阶函数。高阶函数是一种接受函数作为参数或返回函数作为返回值的函数。这种函数可以做很多有趣的事情,例如函数式编程。

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的简介引用被修改。

类比js:js的函数值类型可以看做事值传递,引用类型是引用的指针传递,go里面复制实参的值给形参

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。

package math

func Sin(x float64) float //implemented in assembly language

递归

函数可以是递归的,这意味着函数可以直接或间接的调用自身。对许多问题而言,递归是一种强有力的技术,例如处理递归的数据结构。

类比js:因为go语言里,函数是一等公民,跟js是一样,可以当做参数和返回值一样传递给函数,所以这点上来说是没啥区别的

但是注意的是,go里面的想设置递归的struct的时候,需要用到指针,如下的Node结构体中的FirstChild属性,它要递归Node的话,类型是指针

type Node struct {
    Type                    NodeType
    Data                    string
    Attr                    []Attribute
    FirstChild              *Node
}

多返回值

go中的多返回值其实跟js数组结构功能类似,但是go更偏向于说,多返回值中第二个返回值返回err,也就是错误。

例如,在 Go 中,很多标准库函数都会返回两个值,其中第二个值是 error 类型,用于表示是否发生了错误。

例如:

file, err := os.Open("example.txt")

在这个例子中,函数os.Open()会返回一个指向文件的指针以及一个error,如果文件打开成功,err的值就是nil,否则为错误信息

类比js:这个没啥好说的,跟我们前端的解构赋值差不多

错误

在 Go 中,错误是一种特殊的类型,通常用于报告函数调用中发生的问题。Go 标准库中定义了一个名为 error 的接口类型,该类型包含一个名为 Error 的方法,该方法返回一个字符串,表示错误信息。

在 Go 中,很多函数都会返回一个 error 类型的值,表示函数是否成功执行。如果函数返回的错误为 nil,则表示函数调用成功,否则表示函数调用失败,错误为非nil,则表示函数调用失败,错误信息通过 error 类型的值返回。

例如,下面的代码使用 ioutil.ReadFile 函数读取文件,并判断函数调用是否成功:

content, err := ioutil.ReadFile("example.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(content))

如果文件读取成功,err 的值为 nil,代码将继续执行。否则,调用 log.Fatal(err) 终止程序并打印错误信息。

类比js:这个最好跟node类比,node也是错误优先,尤其调用一些io操作,go基本上也是这样的思想,所以前端可以借鉴这种思想,这种写法好像也有同学写过文章,大概意思是,如果返回没有错误就返回 [null, data],如果有错误就返回[err, null] (也就是数组第一项代表error,第二项代表数据),这个思想可以用在async函数上

函数值

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。例子如下:

func square(n int) int { return n * n }

func negative(n int) int { return -n }

func product(m, n int) int { return m * n }

f := square

fmt.Println(f(3)) // "9"

f = negative

fmt.Println(f(3)) // "-3"

fmt.Printf("%T\n", f) // "func(int) int"

f = product // compile error: can't assign func(int, int) int to func(int) int

函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

var f func(int) int

f(3) // 此处f的值为nil, 会引起panic错误

函数值可以与nil比较。

类比js: 这个没啥特别的,跟ts的函数定义一样

匿名函数

在 Go 中,匿名函数是一种特殊的函数,它没有名称。匿名函数可以赋值给一个变量,也可以直接调用。

匿名函数的语法如下:

func(参数列表)(返回值列表){
    函数体
}

例如,下面是一个匿名函数,该函数接受两个 int 类型参数并返回它们的和:

func(a, b int) int {
    return a + b
}

这个函数可以赋值给一个变量并调用,如下:

add := func(a, b int) int {
    return a + b
}
result := add(1, 2)

类比js:跟js差不多,也有匿名函数,用法也差不多。

匿名函数在 Go 中有很多用途。例如,可以使用匿名函数来实现回调函数,也可以在函数中创建并直接调用匿名函数,还可以将匿名函数作为参数传递给另一个函数。

例如,下面是一个使用匿名函数实现回调函数的例子:

package main

import "fmt"

func visit(numbers []int, callback func(int)) {
    for _, n := range numbers {
        callback(n)
    }
}

func main() {
    visit([]int{1, 2, 3, 4}, func(n int) {
        fmt.Println(n)
    })
}

上面的代码定义了一个名为 visit 的函数,该函数接受一个整型切片和一个回调函数。visit 函数会遍历切片中的所有整数并调用回调函数。在这个例子中,我们传递了一个匿名函数给 visit 函数,该函数会打印每个整数。

可变参数

在 Go 中,可变参数是指一种特殊类型的函数参数,它可以接受任意多个参数。可变参数通常使用 "..." 来表示,它表示函数可以接受任意数量的该类型的参数。

可变参数可以在定义函数时使用,例如:

func myFunction(firstArg string, args ...int) {
    // body of the function
}

在这个例子中, myFunction 接受第一个参数是字符串类型的 firstArg, 后面有任意个整数参数, 这些整数参数被打包成一个int的slice类型的args传入

类比js:我去,这不就是我们js里的剩余参数语法吗,也是...,哈哈,搞定!

可变参数可以在调用函数时传入任意数量的参数,如:

myFunction("Hello", 1, 2, 3)
myFunction("Hello", 1, 2, 3, 4, 5)
myFunction("Hello")

在函数体内部,可变参数被打包成一个切片类型的变量,可以像操作切片一样使用这个变量。

另外需要注意的是,可变参数只能放在函数参数的最后一个位置,在其他位置会报错。

类比js,我们的剩余参数也是转成了一个数组,js数组就是go里面的切片,又是一样的,爽,去下个知识点喽

Deferred函数

在 Go 中,defer 是一个语言级别的关键字,它可以用来推迟函数的执行。使用 defer 时,需要在函数中调用一个名为 defer 的函数,并传入需要推迟执行的函数。这个函数会在该函数返回之前执行。

defer 函数通常用来做资源清理工作,例如关闭文件、释放锁、释放内存等。例如,下面的例子使用 defer 来关闭文件:

Copy code
package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("file.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()

	// Do something with the file
	// ...
}

在这个例子中,我们使用 defer 来关闭文件,defer 函数会在 main 函数返回之前执行。这样做的好处是即使在处理文件时发生了错误,文件也会被关闭,而不会因为没有关闭文件而导致资源泄露。

总结一下:

你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

类比js: node里还真没有这种语法,process.nextTick() 来实现类似的效果,但process.nextTick()是在当前事件循环结束后执行,跟当前函数关系不大,但好像也没人这么用,我猜想是因为node的api本身会自动帮我们释放资源

Panic错误

Panic是 Go 语言中的一种特殊类型的异常,它可以在函数中通过内置函数 panic() 抛出。Panic异常会导致程序立即终止执行,并导致栈追溯。

Panic异常通常用来处理不可恢复的错误,例如空指针引用、越界访问数组、分配内存失败等。

举个例子,如果函数中发现了一个不可恢复的错误,可以使用 panic() 函数来抛出异常:

package main

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

当调用这个函数并传入 b=0 时,函数会抛出一个"division by zero"的异常。这个异常会导致程序终止并产生一个栈追溯(stack trace),栈追溯中包含了产生 Panic 异常的函数的名称和行号,以及调用该函数的函数的名称和行号。

当 panic 异常被触发时,Go 会立即终止当前函数的执行,并返回调用栈上的函数中。这个过程会一直持续直到程序中有一个函数被调用了 recover() 函数为止。

recover() 函数可以用来捕获 panic 异常并处理它,例如通过打印错误信息来记录错误日志。使用 recover() 函数可以避免程序因为未处理的 panic 异常而终止。

需要注意的是,recover 函数只能在 defer 中调用,因为它需要在 panic 异常被触发之前就被调用。

方法

从90年代早期开始,面向对象编程(OOP)就成为了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对OOP的支持,go语言也不例外。

基于指针对象的方法声明

在 Go 中,方法是一种特殊的函数,它是绑定在某个类型上的。在 Go 中,方法的声明格式如下:

func (receiver type) methodName(args) returnType {
    // method body
}

其中,receiver 是方法的接收者,它可以是一个类型的变量或指针,methodName 是方法名称,args 是方法参数,returnType 是方法返回值。

例如,下面是一个定义了一个名为 Employee 的结构体类型和一个名为 RaiseSalary 的方法的例子:

package main

import "fmt"

type Employee struct {
    name string
    salary int
}

func (e *Employee) RaiseSalary(percent int) {
    e.salary = e.salary + e.salary*percent/100
}

在上面的例子中,Employee是一个结构体类型, RaiseSalary 是一个方法,它的接收者是一个 Employee 类型的指针,方法名为 RaiseSalary ,参数为一个整型 percent,没有返回值.

它可以通过指针或值来调用,如下:

    e1:= Employee{"Mark",1000}
    e1.RaiseSalary(10)
    fmt.Println(e1.salary)
    e2:= &Employee{"John",1000}
    e2.RaiseSalary(10)
    fmt.Println(e2.salary)

在这个例子中,e1 是一个值,而 e2 是一个指针,两者都可以调用 RaiseSalary 方法。

类比js: js的对象既可以定义属性,又可以定义方法,但是go的结构体只能定义属性,方法要单独拎出来,通过写明接收体的方式来关联到结构体上

通过嵌入结构体来扩展类型

在 Go 中,可以通过嵌入结构体来扩展类型。这种方法类似于继承,但在 Go 中不存在继承关系。

嵌入结构体的语法如下:

type OuterType struct {
    InnerType
    // other fields
}

例如,下面是一个使用嵌入结构体来扩展类型的例子:

package main

import "fmt"

type Person struct {
    Name string
    Age int
}

type Employee struct {
    Person
    Salary int
}

func main() {
    e := Employee{
        Person: Person{
            Name: "Mark",
            Age: 30,
        },
        Salary: 5000,
    }
    fmt.Println(e.Name)
    fmt.Println(e.Age)
    fmt.Println(e.Salary)
}

在这个例子中, Person 是一个结构体类型, Employee 是另一个结构体类型,它嵌入了 Person 类型。因此, Employee 类型也包含了 Person 类型中的所有字段。

嵌入结构体还可以用来实现类似继承的行为,例如重写嵌入类型中的方法或实现接口。

类比js: js的calss语法是用extends语法实现继承的,本质是通过原型链来实现,是一种纵向的继承,而go没有原型链,类似于设计模式里的桥接模式,或者说组合实现的继承,其实编程原理主流的就那些,一通百通,都差不多

封装

Go语言提供了一种名为封装的特性,其目的是隐藏类型或结构体中的细节,并提供一组接口来控制对它们的访问。在Go中,封装是通过大写字母开头的字段或方法来实现的。小写字母开头的字段或方法被视为私有的,只能在包内部访问。而大写字母开头的字段或方法被视为公开的,可以在包外部访问。

举个例子,我们可以定义一个名为Car的结构体来表示汽车,其中包含了一些私有字段,如引擎类型、油量等,以及一些公开的方法,如启动、停止等。

type Car struct {
    make string
    model string
    year int
    engineType string
    fuelLevel float64
}

func (c *Car) Start() {
    fmt.Println("The car has started.")
}

func (c *Car) Stop() {
    fmt.Println("The car has stopped.")
}

func (c *Car) Drive() {
    fmt.Println("The car is now being driven.")
}

在这个例子中,我们将结构体中的makemodelyearengineTypefuelLevel字段都设置为私有的。只能通过公有的方法来访问这些字段。

这样的好处是,我们可以在包外部访问结构体的公有方法,而不能直接访问结构体的私有字段。这样可以有效地保护结构体内部的状态,并确保对结构体的操作是正确和一致的。

类比js,这就是js的问题,类里面不能声明私有变量,新的提案好像是用# 号,封装是非常有必要区分私有和共有变量的,不过ts解决了这个问题,哈哈

接口

类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。

很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。

接口类型

在 Go 中,接口是一种类型,它定义了一组方法。如果一个类型实现了接口中定义的所有方法,那么这个类型就可以被视为实现了该接口。

接口的语法如下:

type InterfaceName interface {
    MethodName1(parameter list) return type
    MethodName2(parameter list) return type
    ...
}

例如,下面是一个简单的接口示例:

package main

import "fmt"

type Shape interface {
    Area() float64
}

type Rectangle struct {
    width, height float64
}

func (r Rectangle) Area() float64 {
    return r.width * r.height
}

func main() {
    r := Rectangle{10, 5}
    var s Shape = r
    fmt.Println("Area of rectangle:", s.Area())
}

在这个例子中, Shape 是一个接口,它定义了一个 Area 方法。Rectangle 是一个结构体类型,它实现了 Shape 接口中定义的 Area 方法。因此,可以将一个 Rectangle 类型的变量赋值给一个 Shape 类型的变量。

Go 中的接口是隐式的,这意味着不需要显式地声明一个类型实现了某个接口,只要实现了接口中的所有方法就可以认为实现了接口

接口还可以用于类型断言和类型转换,以及依赖注入等设计模式。

除此之外,接口也可以包含已实现的方法,也可以定义一个空接口,这样任何类型都是它的实现类型。

接口还没写完,但是我看文字已经是9千500字左右了,太长不利于阅读,就先写到这,这周末争取写完。