Go学习笔记
1.基础
Go程序设计的一些规则
Go之所以会那么简洁,是因为它有一些默认的行为:
- 大写字母开头的变量是可导出的,也就是其它包可以读取 的,是公用变量;小写字母开头的就是不可导出的,是私有变量。
- 大写字母开头的函数也是一样,相当于class 中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。
for
Go里面最强大的一个控制逻辑就是for,它即可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操
作。它的语法如下:
for expression1; expression2; expression3 {
//...
}
expression1、expression2和expression3都是表达式,其中expression1和expression3是变量声明或者
函数调用返回值之类的,expression2是用来条件判断,expression1在循环开始之前调用,expression3在每
轮循环结束之时调用。
一个例子比上面讲那么多更有用,那么我们看看下面的例子吧:
package main
import "fmt"
func main() {
sum := 0;
for index:=0; index < 10 ; index++ {
sum += index
}
fmt.Println("sum is equal to ", sum)
}
// 输出:sum is equal to 45
有些时候需要进行多个赋值操作,由于Go里面没有,操作,那么可以使用平行赋值i, j = i+1, j-1
有些时候如果我们忽略expression1和expression3:
sum := 1
for ; sum < 1000; {
sum += sum
}
其中;也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是while的功能。
sum := 1
for sum < 1000 {
sum += sum
}
在循环里面有两个关键操作break和continue ,break操作是跳出当前循环,continue是跳过本次循环。当嵌套
过深的时候,break可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子:
for index := 10; index>0; index-- {
if index == 5{
break // 或者continue
}
fmt.Println(index)
}
// break打印出来10、9、8、7、6
// continue打印出来10、9、8、7、6、4、3、2、1
break和continue还可以跟着标号,用来跳到多重循环中的外层循环
for配合range可以用于读取slice和map的数据:
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃
不需要的返回值 例如
for _, v := range map{
fmt.Println("map's val:", v)
}
switch
有些时候你需要写很多的if-else来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维 护,这个时候switch就能很好的解决这个问题。它的语法如下
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
sExpr和expr1、expr2、expr3的类型必须一致。Go的switch非常灵活,表达式不必是常量或整数,执行的过程
从上至下,直到找到匹配项;而如果switch没有表达式,它会匹配true。
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}
在第5行中,我们把很多值聚合在了一个case里面,同时,Go里面switch默认相当于每个case最后带有break,匹
配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代
码。
integer := 6
switch integer {
case 4:
fmt.Println("The integer was <= 4")
fallthrough
case 5:
fmt.Println("The integer was <= 5")
fallthrough
case 6:
fmt.Println("The integer was <= 6")
fallthrough
case 7:
fmt.Println("The integer was <= 7")
fallthrough
case 8:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}
上面的程序将输出
The integer was <= 6
The integer was <= 7
The integer was <= 8
default case
字符串
我们在上一节中讲过,Go中的字符串都是采用UTF-8字符集编码。字符串是用一对双引号(“”)或反引号(
)括
起来定义,它的类型是string。
//示例代码
var frenchHello string // 声明变量为字符串的一般方法
var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串
func test() {
no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量
japaneseHello := "Ohaiou" // 同上
frenchHello = "Bonjour" // 常规赋值
}
在Go中字符串是不可变的,例如下面的代码编译时会报错:
var s string = "hello"
s[0] = 'c'
但如果真的想要修改怎么办呢?下面的代码可以实现:
s := "hello"
c := []byte(s) // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)
Go中可以使用+操作符来连接两个字符串:
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)
修改字符串也可写为:
s := "hello"
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n", s)
如果要声明一个多行的字符串怎么办?可以通过`来声明:
m := `hello
world`
//` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。
slice
在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需 要“动态数组”。在Go里面这种数据结构叫slice
slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array,slice的声明也可以像 array一样,只是不需要长度。
对于slice有几个有用的内置函数:
- len 获取slice的长度 cap 获取slice的最大容量
- append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
- copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数
注:append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原 数组的内容将保持不变;其它引用此数组的slice则不受影响。
package main
import "fmt"
func main() {
nums := []int{1, 2, 3}
nums = append(nums, 4) // 添加元素
nums = append(nums, 5) // 添加元素
fmt.Printf("nums的长度是:", len(nums))
fmt.Printf("nums的最大容量是:", cap(nums))
//nums[0] = 5
fmt.Println(nums)
for _, num := range nums {
fmt.Println(num)
}
//fmt.Println(nums[0:3])// 切片
// 删除索引为2个元素(本质是取索引为2的元素前面的和索引为三开始的)
nums = append(nums[:2], nums[3:]...)
fmt.Println(nums) // 切片
}
map
package main
type student struct {
id string
}
func main() {
var map1 = make(map[string]student, 10)
map1["123"] = student{"123"}
map1["124"] = student{"124"}
map1["125"] = student{"19999"}
println(len(map1))
delete(map1, "126")
println(len(map1))
student, ok := map1["125"]
if ok {
println(student.id)
}
}
make、new操作
make用于内建类型(map、slice 和channel)的内存分配。
new用于各种类型的内存分配。 内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其 地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要: new返回指针。
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初 始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被 初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。
make返回初始化后的(非零)值。
错误类型
Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误:
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
iota枚举
Go里面有一个关键字iota,这个关键字用来声明enum的时候采用,它默认开始值是0,每调用一次加1:
const(
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)
const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0
goto
Go有goto语句——请明智地使用它。用goto跳转到必须在当前函数内定义的标签。例如假设这样一个循环:
func myFunc() {
i := 0
Here: //这行的第一个词,以冒号结束作为标签
println(i)
i++
goto Here //跳转到Here去
}
标签名是大小写敏感的。
2.函数
特性
函数是Go里面的核心设计,它通过关键字func来声明,它的格式如下
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
上面的代码我们看出
- 关键字func用来声明一个函数funcName
- 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔
- 函数可以返回多个值
- 上面返回值声明了两个变量output1和output2,如果你不想声明也可以,直接就两个类型
- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
- 如果没有返回值,那么就直接省略最后的返回信息
- 如果有返回值, 那么必须在函数的外层添加return语句
下面我们来看一个实际应用函数的例子(用来计算Max值)
package main
import "fmt"
// 返回a、b中最大值.
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) //调用函数max(x, y)
max_xz := max(x, z) //调用函数max(x, z)
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它
}
上面这个里面我们可以看到max函数有两个参数,它们的类型都是int,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返 回值就是一个类型,这个就是省略写法。
多个返回值
Go语言比C更先进的特性,其中一点就是函数能够返回多个值。
变参
Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参: func myfunc(arg ...int) {}
arg ...int
告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一 个int的slice:
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
传值与传指针
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候, 调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a int) int {
a = a+1 // 我们改变了a的值
return a //返回一个新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(x) //调用add1(x)
fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4"
fmt.Println("x = ", x) // 应该输出"x = 3"
}
看到了吗?虽然我们调用了add1函数,并且在add1中执行a = a+1操作,但是上面例子中x变量的值没有发生变化
理由很简单:因为当我们调用add1的时候,add1接收的参数其实是x的copy,而不是x本身。 那你也许会问了,如果真的需要传这个x本身,该怎么办呢?
这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内 存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以我们需要将x所在地址&x传入函数,并将函数 的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只 是copy的是一个指针。请看下面的例子
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}
传指针有什么好处呢?
-
传指针使得多个函数能操作同一个对象。
-
传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的 话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用 指针是一个明智的选择。
-
Go语言中string,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递 指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
defer
Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些 defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回, 在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样 操作的:
func ReadWrite() bool {
file.Open("file")
// 做一些工作
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
我们看到上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变 得更优雅。在defer后指定的函数会在函数退出前调用。
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
如果有很多调用defer,那么defer是采用后进先出模式,所以如下代码会输出4 3 2 1 0
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
函数作为值、类型
在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型
函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递
package main
import "fmt"
type testInt func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main() {
slice := []int{1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println("Even elements of slice are: ", even)
}
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类 型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活
Panic和Recover
Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为 最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?
Panic
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中 断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一 过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产 生。也可以由运行时错误产生,例如访问越界的数组。
Recover
是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常 的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用 recover可以捕获到panic的输入值,并且恢复正常的执行。
下面这个函数演示了如何在过程中使用panic
package main
import (
"fmt"
)
func readSliceElement(slice []int, index int) {
// 最后一个()表示立即调用该匿名函数
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
if index < 0 || index >= len(slice) {
panic("Index out of range")
}
fmt.Println("Element at index", index, ":", slice[index])
}
func main() {
slice := []int{1, 2, 3, 4, 5}
// 正常读取
readSliceElement(slice, 2)
// 读取越界的索引
readSliceElement(slice, 10)
}
main函数和init函数
Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。 这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对 于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。
Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是 可选的,但package main就必须包含一个main函数。
程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包 中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕 了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执 行main函数。
3.struct类型
strut
Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以 创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称struct。如下代码所示:
type person struct {
name string
age int
}
看到了吗?声明一个struct如此简单,上面的类型包含有两个字段
- 一个string类型的字段name,用来保存用户名 称这个属性
- 一个int类型的字段age,用来保存用户年龄这个属性
如何使用struct呢?请看下面的代码
type person struct {
name string
age int
}
var P person // P现在就是person类型的变量了
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
P.age = 25 // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.
除了上面这种P的声明使用之外,还有两种声明使用方式
1.按照顺序提供初始化值P := person{"Tom", 25}
2.通过field:value
的方式初始化,这样可以任意顺序P := person{age:24, name:"Tom"}
下面我们看一个完整的使用struct的例子
package main
import "fmt"
// 声明一个新的类型
type person struct {
name string
age int
}
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
// struct也是传值的
func Older(p1, p2 person) (person, int) {
if p1.age > p2.age { // 比较p1和p2这两个人的年龄
return p1, p1.age - p2.age
}
return p2, p2.age - p1.age
}
func main() {
var tom person
// 赋值初始化
tom.name, tom.age = "Tom", 18
// 两个字段都写清楚的初始化
bob := person{age: 25, name: "Bob"}
// 按照struct定义顺序初始化值
paul := person{"Paul", 43}
tb_Older, tb_diff := Older(tom, bob)
tp_Older, tp_diff := Older(tom, paul)
bp_Older, bp_diff := Older(bob, paul)
fmt.Printf("Of %s and %s, %s is older by %d years\n",
tom.name, bob.name, tb_Older.name, tb_diff)
fmt.Printf("Of %s and %s, %s is older by %d years\n",
tom.name, paul.name, tp_Older.name, tp_diff)
fmt.Printf("Of %s and %s, %s is older by %d years\n",
bob.name, paul.name, bp_Older.name, bp_diff)
}
struct的匿名字段
我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字 段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
让我们来看一个例子,让上面说的这些更具体化
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}
func main() {
// 我们初始化一个学生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 我们访问相应的字段
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His speciality is ", mark.speciality)
// 修改对应的备注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改他的年龄信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改他的体重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}
我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字 段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代 码,是不是更酷了。
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名 字段的。请看下面的例子
package main
import "fmt"
type Skills []string
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,struct
Skills // 匿名字段,自定义的类型string slice
int // 内置类型作为匿名字段
speciality string
}
func main() {
// 初始化学生Jane
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
// 现在我们来访问相应的字段
fmt.Println("Her name is ", jane.name)
fmt.Println("Her age is ", jane.age)
fmt.Println("Her weight is ", jane.weight)
fmt.Println("Her speciality is ", jane.speciality)
// 我们来修改他的skill技能字段
jane.Skills = []string{"anatomy"}
fmt.Println("Her skills are ", jane.Skills)
fmt.Println("She acquired two new ones ")
jane.Skills = append(jane.Skills, "physics", "golang")
fmt.Println("Her skills now are ", jane.Skills)
// 修改匿名内置类型字段
jane.int = 3
fmt.Println("Her preferred number is", jane.int)
}
从上面例子我们看出来struct不仅仅能够将struct作为匿名字段、自定义类型、内置类型都可以作为匿名字段,而且 可以在相应的字段上面进行函数操作(如例子中的append)。
这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过student.phone访问的时候,是访问 student里面的字段,而不是human里面的字段。
这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通 过匿名字段名来访问。请看下面的例子
package main
import "fmt"
type Human struct {
name string
age int
phone string // Human类型拥有的字段
}
type Employee struct {
Human // 匿名字段Human
speciality string
phone string // 雇员的phone字段
}
func main() {
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
fmt.Println("Bob's work phone is:", Bob.phone)
// 如果我们要访问Human的phone字段
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}
4.面向对象
method
在使用method的时候重要注意几点
-
虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
-
method里面可以访问接收者的字段
-
调用method通过.访问,就像struct里面访问字段一样
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width * r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
method可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
type typeName typeLiteral
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有s点类似于c中的typedef,例如上面ages替代了int
你可以在任何的自定义类型中定义任意多的method
package main
import "fmt"
const (
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte // Color作为byte的别名
type Box struct { // 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性
width, height, depth float64
color Color
}
type BoxList []Box //定义了一个slice:BoxList,含有Box+
// Volume (b Box)定义了接收者为Box,返回Box的容量
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
// SetColor (c Color),把Box的颜色改为c
func (b *Box) SetColor(c Color) {
b.color = c
}
// BiggestsColor ()定在在BoxList上面,返回list里面容量最大的颜色
func (bl BoxList) BiggestsColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if b.Volume() > v {
v = b.Volume()
k = b.color
}
}
return k
}
// PaintItBlack ()把BoxList里面所有Box的颜色全部变成黑色
func (bl BoxList) PaintItBlack() {
for i, _ := range bl {
bl[i].SetColor(BLACK)
}
}
// String()定义在Color上面,返回Color的具体颜色(字符串格式)
func (c Color) String() string {
strings := []string{"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main() {
boxes := BoxList{
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is", boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String())
}
method继承
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // 匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
func (h *Human) SayHi() {
fmt.Printf("My name is %s,and my phoneNumber is %s\n", h.name, h.phone)
}
func main() {
stu := Student{Human{"张三", 18, "123456"}, "中北大学"}
//emp := Employee{Human{"李四", 19, "987645"}, "字节跳动"}
// 这样写不用写出Human对象
var emp Employee
emp.name = "李四"
emp.age = 19
emp.phone = "987654"
emp.company = "ByteDance"
stu.SayHi()
emp.SayHi()
}
method重写
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // 匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
func (h *Human) SayHi() {
fmt.Printf("My name is %s,and my phoneNumber is %s\n", h.name, h.phone)
}
func (s *Student) SayHi() {
fmt.Printf("My name is %s,and I study at %s\n", s.name, s.school)
}
func (e Employee) SayHi() {
fmt.Printf("My name is %s,and I work at %s\n", e.name, e.company)
}
func main() {
stu := Student{Human{"张三", 18, "123456"}, "中北大学"}
emp := Employee{Human{"李四", 19, "987645"}, "字节跳动"}
stu.SayHi()
emp.SayHi()
}
5.interface
interface
interface本质是个指针
interface是一组method的组合,我们通过interface来定义对象的一组行为。
我们前面一章最后一个例子中Student和Employee都能Sayhi,虽然他们的内部实现不一样,但是那不重要,重要的是 他们都能say hi
让我们来继续做更多的扩展,Student和Employee实现另一个方法Sing,然后Student实现方法BorrowMoney而 Employee实现SpendSalary。
这样Student实现了三个方法:Sayhi、Sing、BorrowMoney;而Employee实现了Sayhi、Sing、SpendSalary。
上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了interface: Sayhi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:Sayhi、Sing和 BorrowMoney,因为Employee没有实现BorrowMoney这个方法。
interface类型
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段Human
school string
loan float32
}
type Employee struct {
Human //匿名字段Human
company string
money float32
}
// Human对象实现Sayhi方法
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Human对象实现Sing方法
func (h *Human) Sing(lyrics string) {
fmt.Println("La la, la la la, la la la la la...", lyrics)
}
// Human对象实现Guzzle方法
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// Employee重载Human的Sayhi方法
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
// Student实现BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (again and again and...)
}
// Employee实现SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount // More vodka please!!! Get me through the day!
}
// 定义interface
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
type YoungChap interface {
SayHi()
Sing(song string)
BorrowMoney(amount float32)
}
type ElderlyGent interface {
SayHi()
Sing(song string)
SpendSalary(amount float32)
}
interface值
即多态
如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个 interface的任意类型的对象。例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存 Human、Student或者Employee值。
因为m能够持有这三种类型的对象,所以我们可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了 Men接口的任意结构的对象,这个和我们传统意义上面的slice有所不同。
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
loan float32
}
type Employee struct {
Human //匿名字段
company string
money float32
}
// Human实现Sayhi方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Human实现Sing方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
// Employee重载Human的SayHi方法
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}
//定义Men类型的变量i
var i Men
//i能存储Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i也能存储Employee
i = Tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定义了slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//T这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}
通过上面的代码,你会发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实 现, go 通过interface实现了duck-typing:即"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那 么这只鸟就可以被称为鸭子"。
空interface
空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于 描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s
一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也 就可以返回任意类型的值。是不是很有用啊!
package main
import "fmt"
func show(arg interface{}) {
//fmt.Println(arg)
// interface{} 如何区分此时引用的底层数据类型到底是什么?
// 类型“断言”机制
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value = ", value)
}
}
type Book struct {
name string
}
func main() {
book := Book{"Golang"}
show(book)
show(100)
show("abc")
show(3.14)
}
interface函数参数
6.反射
基本用法
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 3.14159
reflectNum(num)
}
func reflectNum(arg interface{}) {
fmt.Println("type:", reflect.TypeOf(arg))
fmt.Println("value:", reflect.ValueOf(arg))
}
package main
import (
"fmt"
"reflect"
)
type User struct {
Id int
Name string
Age int
}
func (u User) Call() {
fmt.Println("user is called..")
fmt.Printf("%v\n", u)
}
func main() {
user := User{1, "Tom", 20}
DoFiledAndMethod(user)
}
func DoFiledAndMethod(input interface{}) {
// 获取input的type
inputType := reflect.TypeOf(input)
fmt.Println("inputType is :", inputType)
// 获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue is :", inputValue)
//通过type 获取里面的字段
//1.获取interface的reflect.Type,通过Type得到NumFiled,进行遍历
//2.得到每一个field,数据类型
//3.通过field有一个Interface{}方法得到对应的value
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
//通过type 获取里面的方法,调用
for i := 0; i < inputType.NumMethod(); i++ {
method := inputType.Method(i)
fmt.Printf("%s: %v\n", method.Name, method.Type)
}
}
反射解析结构体标签Tag
package main
import (
"fmt"
"reflect"
)
type resume struct {
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}
func findTag(str interface{}) {
t := reflect.TypeOf(str).Elem()
for i := 0; i < t.NumField(); i++ {
tagString := t.Field(i).Tag.Get("info")
tagDoc := t.Field(i).Tag.Get("doc")
fmt.Println("info:", tagString, " doc:", tagDoc)
}
}
func main() {
var re resume
findTag(&re)
}
结构体标签在json中的应用
package main
import (
"encoding/json"
"fmt"
)
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"price"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"xingye", "zhangbozhi"}}
// 编码的过程 结构体 ---> json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error")
return
}
fmt.Printf("jsonStr= %s\n", jsonStr)
// 解码的过程 jsonStr ---> 结构体
// jsonStr= {"Title":"喜剧之王","year":2000,"price":10,"actors":["xingye","zhangbozhi"]}
my_movie := Movie{}
err = json.Unmarshal(jsonStr, &my_movie)
if err != nil {
fmt.Println("json unmarshal error")
return
}
fmt.Printf("%v", my_movie)
}
7.并发
基础
进程和线程
进程占用内存 虚拟内存4GB(32Bit OS)
线程占用内存 约4MB
缺点:高消耗调度CPU、高内存占用
协程
Golang对协程的处理:Goroutine
- 内存:几KB(可以大量)
- 灵活调度(可常切换)
GMP
- G:goroutine协程
- P:processor处理器
- M:thread线程
调度器的设计策略
-
复用线程
-
空闲的P可以偷有别的P中等待的G(work stealing)
-
一个M阻塞之后,M和P会取消绑定,然后P绑定一个新的M
-
-
利用并行
- 限定P的个数:一般是CPU核数/2
-
抢占:
-
绑定之后,需要co-routine主动释放CPU(旧版co-routine),
-
最多轮询10ms,否则新的G一定会抢占CPU(goroutine)
-
-
全局G队列
- 优先去其他队列偷,也没有的话去全局队列获取(涉及加锁解锁)
创建goroutine
函数
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
func main() {
go newTask()
// main线程在才会创建协程
fmt.Println("main goroutine exit")
/*i := 0
for {
i++
fmt.Printf("main goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}*/
}
匿名函数
package main
import (
"fmt"
"time"
)
func main() {
// 用go创建承载一个形参为空,返回值为空的一个函数
/*go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 退出当前goroutine,用return只能退出当前的,无法退出外层的
runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("A")
}()*/
go func(a int, b int) bool {
fmt.Println("a =", a, "b =", b)
return true
}(10, 20)
//死循环
for {
time.Sleep(1 * time.Second)
}
}
channel
用于goroutine之间通信
无缓冲的channel
- 在第1步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。
- 在第2步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
- 在第3步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个goroutine 一样也会在通道中被锁住,直到交换完成。
- 在第4 步和第5步,进行交换,并最终,在第6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做其他事情了。
package main
import "fmt"
func main() {
// 定义一个无缓存的channel,最多有一个元素,类型为int,
c := make(chan int)
go func() {
defer fmt.Println("goroutine结束")
fmt.Println("goroutine正在运行...")
c <- 666 // 将666发送给c
}()
num := <-c // main goroutine在此处阻塞,等sub goroutine执行完毕后继续
fmt.Println("num =", num)
fmt.Println("main goroutine 结束")
}
有缓冲的channel
- 在第1步,右侧的 goroutine 正在从通道接收一个值。
- 在第2步,右侧的这个 goroutine独立完成了接收值的动作,而左侧的goroutine正在发送一个新值到通道里
- 在第3步,左侧的goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
- 最后,在第4 步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。
- 特点
- 当channel已经满,再向里面写数据,就会阻塞
- 当channel为空,从里面取数据也会阻塞
package main
import (
"fmt"
"time"
)
func main() {
// 带有缓冲的channel,类型为int,
c := make(chan int, 3)
fmt.Println("len(c) =", len(c), "cap(c) =", cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 4; i++ {
c <- i
fmt.Println("子go程正在运行: 发送的元素=", i, "len(c) =", len(c), ",cap(c) =", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 4; i++ {
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main 结束")
}
关闭channel
- channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
- 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
- 关闭channel后,可以继续从channel接收数据;
- 对于nil channel,无论收发都会被阻塞
package main
import "fmt"
func main() {
// 带有缓冲的channel,类型为int,
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()
for {
// ok:如果为true表示channel没有关闭,为false表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Main Finished")
}
channel与range
package main
import "fmt"
func main() {
// 带有缓冲的channel,类型为int,
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()
/*for {
// ok:如果为true表示channel没有关闭,为false表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}*/
// 可以使用range来迭代不断操作channel
for data := range c {
fmt.Println(data)
}
fmt.Println("Main Finished")
}
channel与select
单流程下一个go只能监控一个channel的状态,select可以完成监控多个channel的状态
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
// 如果c可写,则该case就会进来
x = y
y = x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
8.Go Modules模式
go mod命令
go modinit 生成 go.mod 文件
go mod download 下载 go.mod 文件中指明的所有依赖
go mod tidy 整理现有的依赖
go mod graph 查看现有的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到vendor目录
go mod verify 校验一个模块是否被篡改过
go mod why查看为什么需要依赖某模块
go mod 环境变量
-
GO111MODULE
- 是否开启go modules模式
- 建议go V1.11之后都设置为on
-
GOPROVY
- 项目的第三方依赖库的下载源地址
- 建议设置国内的地址
- 阿里云:https://mirrors.aliyun.com/goprovy
- 七牛云:https://goproxy.cn,direct
- direct:用于指示Go回源到模块版本的源地址去抓取(比如github等)
-
GOSUMDB
- 用来校验拉取的第三方库是否是完整的
- 默认也是国外的网站,如果设置了GOPROXY,这个就不用设置了
-
GONOPROXY:通过设置GOPRIVATE即可
-
GONOSUMDB:通过设置GOPRIVATE即可
-
GOPRIVATE:通过设置GOPRIVATE即可
-
go env -w GOPRIVATE="git.example.com,github.com/accld/zinx"
表示git.example.com
和github.com/accld/zinx
是私有仓库,不会进行GOPROXY下载和校验 -
go env -w GOPRIVATE="*.example.com"
表示所有模块路径为example.com
的子域名,比如git.example.com
或者hello.example.com
都不进行GOPROXY下载和校验
-
-
通过
go env
来查看环境变量- go env -w GO111MODULE=on
- 或者通过Linux export环境方式也可以
go mod
//indirect表示间接依赖
go sum
go.sum文件的作用:罗列当前项目直接或者间接的依赖所有模块版本,保证今后项目依赖的版本不会被篡改
h1:hash
表示整体项目的zip文件打开之后的全部文件的校验和来生成的hash,如果不存在,表示依赖的库可能用不上
9.生态
WEB
-
国内
- beego:https://github.com/astaxie/beego,适宜初学者
-
国外
- gin:https://github.com/gin-gonic/gin,高性能轻量级框架
- echo:https://github.com/labstack/echo
- Iris:https://github.com/kataras/iris
微服务框架
-
go kit:http://gokit.io,一套集成组成方案,有很多工具,灵活,适合小项目
-
Istio:https://istio.io,一体化集成式,比较全面,适合繁琐的大项目
容器编排
- Kubernetes:https://github.com/kubernetes/kubernetes,google出来,更加通用
- swarm:https://github.com/docker/classicswarm,docker团队开发,docker中集成
服务发现(服务注册)
- consul:https://github.com/hashicorp/consul
存储引擎
- etcd:k/v存储,https://github.com/coreos/etcd,类似redis,比redis好在支持分布式kv存储,一致性做的也不错
- tidb:分布式SQL存储,https://github.com/pingcap/tidb
静态建站
- hugo:https://github.com/gohugoio/hugo
中间件
-
消息队列
- nsq:https://github.com/nsqio/nsq
-
Tcp长连接框架(轻量级服务器)
- zinx:https://github.com/aceld/zinx
-
游戏服务器
- Leaf:https://github.com/name5566/leaf
-
RPC框架文章来源:https://www.toymoban.com/news/detail-828180.html
- gRPC:比较标准的rpc
- https://grpc.io
- https://github.com/grpc/grpc-go
- gRPC:比较标准的rpc
-
Redis集群文章来源地址https://www.toymoban.com/news/detail-828180.html
- codis:https://github.com/CodisLabs/codis
爬虫框架
- go query:https://github.com/PuerkitoBio/goquery
10.WEB
gin+gorm简单使用
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Film struct {
ID uint
Name string
CategoryIDs string
Keywords string
DirectorItems string
Address string
Introduction string
ShowTime string
CreateTime string
UpdateTime string
ClickCount int
ImgURL string
ResourceURL string
IsOn int
LastOperatorID int
}
func main() {
dsn := "root:root@tcp(127.0.0.1:3306)/film_recsys"
// 连接 MySQL 数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 创建默认的gin引擎
r := gin.Default()
// 添加路由处理程序
r.GET("/films", func(c *gin.Context) {
var films []Film
// 查询所有电影
db.Table("film").Find(&films)
// 返回JSON
c.JSON(200, films)
})
// 启动服务器
if err := r.Run(":8080"); err != nil {
fmt.Println("Server failed to start.")
}
}
到了这里,关于Golang快速入门到实践学习笔记的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!