【云+社区年度征文】我是如何 3 小时上手 Golang 的

Golang 是一门诞生 10 来年左右的“新”的编程语言(2009 年开源,相比 C 和 Java 是新语言),但是很多人推荐学习它,因为它的并发编程很方便,还有 Docker 是使用 Golang 开发的。因此我也打算简单了解一下,算是拓宽一下自己的知识面,或者有一天可以用到它。无论怎样吧,我把我学习的内容进行了整理,方便其他有意向的同学可以进行快速了解。

Golang 也被称为 Go,或者 Go 语言。我并没有按照一般的学习路线进行学习,毕竟我已经具备了一些其他编程语言的基础,比如 Java、PHP、C 等。因此,我大体从五个方面对 Go 语言进行了学习,我学习的内容包括 指针、函数参数的传递、面向对象、协程 和 Gin 框架的简单使用。如果读者完全是一个编程小白,就是没有任何编程语言的基础,那么这篇文章并不适合阅读,因为我只是挑了几个自认为的重点进行了学习了解,因此至少要了解一门高级编程语言再看。

接下来,我开始介绍对于 Go 我学习的几个点。

指针

指针被称为 C 语言的灵魂,同时也是被很多非 C 语言程序员望而生畏的一个特性。Java、C# 等语言虽然也会有报出类似“空指针”这样的异常,但是 Java 和 C# 已经在有意的将这个概念淡化掉,甚至不再提它。但是,Go 语言中是有这个概念的。

提到指针,就不得不提 C 语言的指针。C 语言的指针可以简单的被理解为是一个内存地址,但我认为这么理解其实并不严谨。我在学 C 语言的时候,书上和老师都说“指针就是一个地址”,但是我觉得很奇怪,“指针”既然是一个“地址”,为什么不直接了当的说它是一个“地址”,而是要称呼它为“指针”。后来经过自己的使用,好像明白了其中的原因。“地址”表示的是内存的一个编号,而“指针”表示的是一块内存的起始位置。也就是说,“指针”是有类型和长度的。而 指针 的类型关系到了 指针 的 加法 和 减法 的运算。

在 C 语言中,指针常用的运算有 加法、减法、取地址 和 取内容。对应的运算符分别是:自增 1(++)、自减 1(--)、自增(+=)、自减(-=)、取地址(&) 和 取内容(*)。C 语言的指针在进行 加法 和 减法 运算的时候,是根据 指针类型的不同 所移动的地址长度是不同的,这点是很关键的一点,往往这点也是比较难理解的一点,尤其是操作 多级指针 和 多维数组 的时候。不过,好在 C 语言中的 指针自增、自减 的这些操作在 Go 语言中都没有。在 Go 语言中,只有 取地址(&) 和 取内容(*)两个操作,因此,我们只关心 & 和 * 即可。

对于 Go 语言的指针相对于 C 语言的指针而言是非常简单的,这里通过一个简单的例子,来了解一下 Go 语言这两个运算符的使用方法。先来看 取地址 运算符 & 的用法。

package main

import "fmt"

func main() {
	var i int
	i = 10

	fmt.Printf("%x, %d\\r\\n", &i, i)
}

上面的代码中,定义了一个整型变量 i,然后通过 fmt.Printf() 来输出了 变量 i 的地址 和 变量 i 的值。上面的内容输出如下:

src>go run point.go
c0000a2058, 10

上面输出中的 c0000a2058 就是变量 i 的地址(地址的输出会因操作系统版本、补丁版本、Go 编译器版本的不同而不同),因此只要在变量前使用 & 就可以得到变量的地址,看下面的示意图。

变量 i 的内存示意图

上面的变量 i 是一个通常情况下的普通变量,接着我们来看一下 指针变量。我们来定义一个 指针类型 的变量,代码如下:

package main

import "fmt"

func main() {
	var i int
	var p *int

	i = 10
	p = &i

	fmt.Printf("%x, %d\\r\\n", &i, i)
	fmt.Printf("%x, %x, %d\\r\\n", &p, p, *p)
}

在上面的代码中,定义了 整型指针类型 的变量 p,指针变量中存放的是一个内存地址,我们就可以将 i 的地址赋值给 p,又因为 变量 i 是 int 类型,因此需要将 p 定义为 *int 类型。因为 p 是 指向整型的指针,指针中保存的是内存的地址,因此对 p 进行赋值的时候,在 整型 变量前加一个 & 符号,变量 p 中就保存了 i 的地址。上面代码的输出结果如下:

src>go run point.go
c00000a0c0, 10
c000006028, c00000a0c0, 10

上面的输出,第一行输出的是变量 i 的 地址 和 值,第二行的输出,分别是 p 的地址,p 指向的地址 和 p 指向的地址中的值,从指针变量中取出它指向的变量的值,需要使用 * 运算符。对于一个 普通变量 而言,有它的地址,和它的值。而对于一个 指针变量 而言,有它的地址、它指向的地址(其实就是它的值),还有它指向的地址中的值。它们之间的关系大致如下图所示。

指针变量中保存着地址

指针变量在使用之前需要进行初始化,无论是取值、还是赋值,这点和 C 语言一样。不过在 Go 语言中的指针如果没有初始化,直接输出指针变量的值为 0,这点与 C 语言不太相同。但是,在 Go 语言中对未初始化的指针取内容,则和 C 语言中的情况是一样的,即“内存访问违例”。代码如下:

package main

import "fmt"

func main() {
	var ptr *int

	fmt.Printf("%x\\r\\n", ptr)
	fmt.Printf("%d\\r\\n", *ptr)
}

定义了一个 *int 的指针,然后分别打印它的值和从中取内容,运行输出如下:

src>go run point.go
0
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x437074]

goroutine 1 [running]:
main.main()
        E:/index/src/point.go:9 +0x94
exit status 2

从输出可以看出,首先输出一个 0,即输出了 ptr 的值,然后输出 *ptr 时,产生了报错。在 Windows 下,0xc0000005 就表示“内存访问违例”,因为 0 地址是不允许访问的。

Go 的指针不单单只有这些,还有 指针数组、数组指针、多级指针 等,但是对于指针就介绍这些,因为在下面 函数参数的传递 部分同样是介绍指针的使用。

函数参数的传递

Go 语言和其他语言的函数参数传递差不多,也分为 值传递 和 引用传递 两种类型。通常,参数的传递是进行一次内存的拷贝,无论是 值传递,还是 引用传递,实时上都是进行内存的拷贝,只是拷贝的内容有所不同。先来看一段代码:

package main

import "fmt"

func test111(x int, y *int) {
	fmt.Printf("test111 &x = %x, y = %x, &y = %x\\r\\n", &x, y, &y)
	fmt.Printf("test111 x = %d, *y = %d\\r\\n", x, *y)
}

func main() {
	var i int
	i = 10
	fmt.Printf("main i = %d, &i = %x\\r\\n", i, &i)
	test111(i, &i)
}

运行上面的代码,输出如下:

src>go run point.go
main i = 10, &i = c00000a0c0
test111 &x = c00000a0c8, y = c00000a0c0, &y = c000006030
test111 x = 10, *y = 10

上面的代码中,在 main 函数中定义了一个 int 型的变量 i,并给 i 赋值为 10,然后打印输出变量 i 的值和地址,分别为 10 和 c0000a2058。接着调用 test111 函数,该函数接收两个参数,一个是 int 类型,另外一个是 *int 类型,也就是 整型 和 指向整型的指针 两种类型。在 main 函数中,把 i 和 &i 分别传给 test111 函数。然后分别对参数 x 和 y 做输出,观察它们的内存分布。内存分布大致如下图所示。

函数调用中变量(参数)的内存分布

从上面的示意图中可以清楚的看出 main 函数中局部整型变量 i 和 test111 函数中两个参数的关系,已经内存的分布。从此图可以清楚的看出,函数调用时参数到底传递了什么值。

看完上面的例子,我们可以体会到一点,在 test111 中修改了 x 变量的值,是不会影响到 main 函数中 i 的值,但是如果修改 y 所指向的地址中的值,那么就会影响 main 函数中 i 的值。那么,在函数调用时传递指针,除了在 被调函数 中可以修改 主调函数 的值,还有其他优点么?我们来看下面的一段代码,代码如下:

package main

import "fmt"

type stu struct {
	name string
	age  int
}

func test(ts *stu, tps stu) {
	fmt.Println("test---------------")
	fmt.Printf("tps      -> %p, %p, ts      -> %p\\r\\n", tps, &tps, &ts)
	fmt.Printf("tps.name -> %p, %s, ts.name -> %p, %s\\r\\n", &tps.name, tps.name, &ts.name, ts.name)
	fmt.Printf("tps.age  -> %p, %d, ts.age  -> %p, %d\\r\\n", &tps.age, tps.age, &ts.age, ts.age)
	fmt.Println("test---------------")
}

func main() {
	s := stu{}
	s.name = "hehe"
	s.age = 18

	fmt.Println("main---------------")
	fmt.Printf("s      -> %p\\r\\n", &s)
	fmt.Printf("s.name -> %p\\r\\n", &s.name)
	fmt.Printf("s.age  -> %p\\r\\n", &s.age)
	fmt.Println("main---------------")
	test(&s, s)
}

上面的代码中,函数的传递不再是简单的变量,而是一个结构体。我们来看看在函数传参时,传递 结构体 和传递 结构体指针 的输出情况。

main---------------
s      -> 0xc000004480
s.name -> 0xc000004480
s.age  -> 0xc000004490
test---------------
tps      -> 0xc000004480, 0xc000006030, ts      -> 0xc0000044c0
tps.name -> 0xc000004480, hehe, ts.name -> 0xc0000044c0, hehe
tps.age  -> 0xc000004490, 18, ts.age  -> 0xc0000044d0, 18
test---------------

同样的,我们来按照输出内容看一下函数调用之间的内存分布,如下图所示。

函数调用中结构体变量(参数)的内存分布

在图中,main 函数传 s 的指针给 test 函数的 tps 使用的是指针类型,指针类型的传递,只是做了一次 地址值 到 tps 的赋值。而 main 函数传 s 给 test 函数的 ts 使用的是值传递,那么将 s 的各个值做了一次拷贝,拷贝给了 ts 变量。那么可以看出,在函数之间传递 指针(引用)的时候效率会高。

注:字符串的值,并不直接在结构体中存储,结构体中只是存储了指向字符串的指针。

关于函数传参的部分,就整理这么多。接下来开始整理一下关于面向对象的部分。

面向对象

面向对象有几大特性,分别是 封装、继承 和 多态。Go 语言也基本支持,之所以说是基本支持,原因是它并没有类这个概念,而是类似 C 语言的 结构体。而继承在 Go 语言中也没有这个概念,但是它有接口的概念,对于设计模式来讲,更提倡使用面向接口的开发。接下来,我们分别来看看 Go 语言的是如何完成面向对象的几大特点。

封装

在 Go 语言中没有 class 关键字,也就是说无法直接定义一个类出来。但是 Go 语言有一个用来定义结构体的关键字 —— struct,通过 struct 关键字可以将属性进行封装,从而完成和 class 类似的形式。虽然不能在 struct 封装方法,但是可以将 struct 和操作 struct 的函数放到同一个文件下,来达到 class 的效果。定义一个简单的 struct,代码如下:

type Order struct {
	Price int
	cnt int
	total_price int
}

上面的代码中,定义了一个 struct,struct 中有三个属性,分别是 Price、cnt 和 total_price。在属性中,以大写字母开头的属性,类似是一个 public 的属性,public 的属性可以被其他包直接操作,比如 Price 可以被其他包直接操作。而 cnt 和 total_price 是小写字母开头的,那么它只能在包内被访问,包外是无法直接访问的,这样就类似 private 的属性了。对于包内的常量、变量、函数等的访问规则,都要遵循开头字母是否大小写的规则。Go 语言的这个规则,想待遇从语言层面就给程序员定义了代码规范,用心良苦!

那么类似 cnt 和 total_price 是否可以通过其他方式被包外操作呢?答案是肯定的。就类似于 Java 语言中的 getter 和 setter 方法一样。我们在 struct 定义的文件下再来写两个函数,函数的代码如下。

package order

type Order struct {
	Price int
	cnt int
	total_price int
}

func (od *Order) SetCnt(cnt int) {
	od.cnt = cnt
}

func (od Order) CalcTotalPrice () int {
	return od.Price * od.cnt
}

上面的两个函数,在 关键字 func 后面和 函数名前面 多了一部分内容,(order *Order) 和 (od Order) 。这部分在 Go 语言中叫做“接收者”。看着很怪,其实一点也不怪。在我们使用 C++ 是,C++ 的对象属性是独有的,而方法是公有的,那么当调用这个方法时,这个方法是怎么知道操作的是哪个对象的属性呢?答案是 this 指针,也就是说在 C++ 中,对象调用方法时,会隐含的传递一个 this 指针给方法,而这个工作是编译器完成的,程序员无感知。

回到 Go 语言中,我认为这个“接收者”就类似是一个 this 指针。在接收者中,可以直接使用 Order 类型,也可以使用 *Order 类型,有什么区别么?我认为,区别就是在函数中的操作,是否要改变当前对象属性的值,如果要改变就用 *Order,如果不改变那么使用 Order 也可以。

最后补充一句,上面这两个函数是对包外提供的,因此函数名记得要大写。

接口

在 Go 语言中,提供了接口的关键字。接口是一个抽象,在 Go 语言中提倡面向接口编程,这点也比较符合设计模式。因为很多设计模式中都强调,“多用组合,少用继承”。对于接口而言,使用起来和 Java 其实差不多,只是换了一种写法而已。直接上代码即可。

定义一个 GPS 的接口,代码如下。

package gps

type GPS interface {
	Signal()
}

GPS 在手机、汽车中使用比较多,分别在手机和汽车中对它进行实现。先来看汽车类中对 GPS 的实现,代码如下。

package car

import (
	"fmt"
	"object/gps"
)

type Benz struct {
	gps.GPS
}

func (b Benz) Signal() {
	fmt.Println("benz signal")
}

接着实现手机的 GPS 接口,代码如下。

package phone

import (
	"fmt"
	"object/gps"
)

type Mi struct {
	gps.GPS
}

func (m Mi) Signal() {
	fmt.Println("mi signal")
}

我们来分别调用一下这两个实现后的接口,代码如下。

package main

import (
	"object/car"
	"object/phone"
)

func main() {
	m := phone.Mi{}
	m.Signal()

	c := car.Benz{}
	c.Signal()
}

代码输出的结果如下。

go run object.go
mi signal
benz signal

可以看到,我们成功调用了手机的 GPS,还成功调用了汽车的 GPS。

实现接口也可以看作是继承,但是接口中是没有属性的。

注意:Go 语言的包是通过 目录 区分的,我使用的 Go 是 1.15.5 的版本,当我引入我自己的包时,无法使用相对路径,它提示错误。后来找到一条命令,执行以后就可以正常的引入自己写的包了,命令如下:

go mod init object

其中,mod 是用来维护模块的,init 是在当前目录下初始化一个模块的,而 object 就是我起的一个模块的名字。有了名字之后,我们引入自定义包的时候,按照 模块名/包名 引入就好。比如我们代码中的引入方式如下。

import (
	"object/car"
	"object/phone"
)

多态

多态的大体意思就是在运行时决定具体执行的方法。比如,我们在写代码的时候,并不知道用户具体提供的对象,也就无法在编码时确定调用的方法,因此需要使用多态的方式在编码时屏蔽掉变化的部分,去提高代码的抽象能力。我们来看简单的看一段 Go 语言的多态例子代码,代码如下。

package main

import (
	"object/car"
	"object/gps"
	"object/phone"
)

func main() {
	m := phone.Mi{}

	c := car.Benz{}

	gpss := [2]gps.GPS{&m ,&c}

	for n, _ := range gpss {
		gpss[n].Signal()
	}
}

从上面的代码中可以看出,我们调用具体实现时是由接口去调用的,而具体的对象。在代码中,我们定义时去定义抽象,而调用时传入具体的实现,就可以完成多态。这也是面向对象中提倡的,面向抽象编程,而非面向实现编程。

协程

协程算是 Go 语言中真正的一个特性了。协程是类似轻量级的线程,且协程是由 Go 自身的数据结构去管理维护的,而线程是可以真正在 CPU 上运行的,因此协程最后的执行也是被调度到某个具体的线程上去执行的。因为 Go 内部自身去管理,省去了线程切换的开销,以及不受制于操作系统对于线程创建数量的限制。具体的协程模型可以自行参考一下 GMP 模型了解其工作原理。

对于协程,我了解协程的三个方面,由于时间短的关系,我真的没有花太多的时间去好好了解这块。我了解的 Go 创建协程的方式、协程之间的通信 和 协程之间的同步。对于 多进程 和 多线程 编程也是需要了解这些知识的。

对于协程的创建是非常简单的 ,只需要一个 go 关键字就可以创建一个协程,比起 C 语言创建线程要传递多个参数来说真的是方便了许多。协程之间的通信使用 channel 即可,channel 的操作我了解了两个,分别是 <- 和 ->。协程之间的同步,我只了解了一个 sync.WaitGroup 。

这部分我只是完成了一个简单的 Demo,具体看代码吧。

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

var intChan = make(chan int, 5)

func test1() {
	i := 0
	for {
		if i > 100 {
			break
		}
		// 把 i 的值给到 channel
		intChan <- i
		i++
	}

	// 将 -1 送入 chennel
	intChan <- -1
	wg.Done()
}

func test2() {
	for {
		// 从 channel 读取值
		value := <-intChan
		fmt.Println("value: ", value)

		// 当读取到的值为 -1 是退出循环
		if value == -1 {
			break
		}
	}

	wg.Done()
}

func main() {
	wg.Add(2)

	go test1()
	go test2()

	// 等待两个协程的完成后再继续往下执行
	wg.Wait()
	fmt.Println("end..")
}

这个例子是我自己写的一个 Demo,代码中分别用 go 关键字创建了 test1 和 test2 两个协程。test1 和 test2 本身就是两个普通的函数,当在调用 test1 和 test2 前增加关键 go 时,它们就变成了协程。go 创建协程就这么简单。协程 test1 用来循环递增 i 的值,并送给协程 test2,然后值由协程 test2 来进行打印输出。在创建 test1 和 test2 两个协程后,通过 sync.WaitGroup 的 Wait 函数来等待两个协程的完成,然后在执行后学的代码。

上面代码的输出结果如下:

go run waitgroup.go
value:  0
value:  1
value:  2
value:  3
value:  4
value:  5
value:  6
value:  7
value:  8
value:  9
value:  10
value:  11
value:  12

value:  97
value:  98
value:  99
value:  100
value:  -1
end..

由于输出过长,我截取了一部分贴了出来。

对于协程的内容就总结了这么多,毕竟没有深入的学习。

Gin 框架

Gin 框架是一个开源的 Web 框架,它的地址是:https://github.com/gin-gonic/gin 。为什么选择这个框架,因为对于想要快速上手的我来说,哪个框架能快速部署起来,我就用哪个,没有做太多的考虑。

Gin 的安装比较简单,github 上有提供详细的安装方法。下载源码后,需要使用 go mod 来进行初始化,初始化命令如下:

 go mod init gin
 
 go mod edit -require github.com/gin-gonic/gin@latest

先来写一个最简单的 Demo 代码,代码如下。

package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	
	r.Run()
}

在代码中,我们提供了一个通过 get 方式访问的 uri,即 /ping 的 uri。这样就可以让 Gin 框架运行起来,运行的命令如下。

go run index.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

可以看到 Gin 框架监听了 8080 端口号,然后我们用浏览器我们的 uri,如下图所示。

访问 /ping

从图中可以看出,Gin 框架返回了 JSON,而且在我们启动 Gin 的命令行中也有相应的提示,输出如下。

[GIN] 2020/12/07 - 11:41:38 |?[97;42m 200 ?[0m|            0s |             ::1 |?[97;44m GET     ?[0m "/ping"

上面的 Demo 是比较简单的,我们来完成一个可以操作数据库的 Demo。先来看一下目录的结构,如下图所示。

项目目录结构

其中,go.mod 文件是上面 go mod init 命令生成的,只要执行了这个命令,项目目录下都会有这个文件。

来介绍一下目录结构。

controller 目录下放的是关于 Book 表对应的结构体,和对 Book 表的查询。而 controller 目录下的 router.go 是对 BookController.go 下函数的路由。

routers 目录下的 routers.go 文件,是加载路由的文件。

utils 目录下的 db.go 文件,是连接数据的文件。

views 目录下是 html 文件的页面。

index.go 文件是整个项目的启动文件。

先来看看项目的效果,启动项目,命令如下。

go run index.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /book/get/:id             --> gin/controllers.GetBook (1 handlers)
[GIN-debug] GET    /book/hello               --> gin/controllers.View (1 handlers)
[GIN-debug] Loaded HTML Templates (2):
        -
        - test_index.html

[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

从上面可以看到,有两个文明注册的路由,还加载了我们的 html 文件。现在来访问一下我们的路由,如下图所示。

/book/hello
获取book表中id为1的记录

每个文件中的代码不多,我把代码贴到这里文章的最后,方便大家阅读和复制。

总结

本文是一篇 Go 语言上手的文章,所谓的上手,就是有一点基础后,看着例子可以进行简单的照猫画虎而已。这种学习方法我认为是一种比较便捷的学习方法,各个语言的基础语法不外乎分支、循环,这些都没有太多必要花时间学习,无非就是换了种写法,或者是各种语法糖。而类库一个积累的过程。当然啦,要想学好 Go 语言,还是要认证的去学习一遍。

Go 语言初学者,写作难免有误,如有问题,望指正。

附:Gin 框架 Demo 源码

BookController.go 的代码如下。

package controllers

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"

	"gin/utils"
)

type Book struct {
	Id        int
	Book_name string
	Price     float32
}

func (book *Book) query(id string) Book {
	b := Book{}

	Db := utils.GetMySQLConnection()

	row := Db.QueryRow("select * from book where id = ?;", id)

	e := row.Scan(&b.Id, &b.Book_name, &b.Price)

	if e != nil {
		log.Panicln(e)
	}

	fmt.Println(b)
	fmt.Println("query...")

	return b
}

func GetBook(c *gin.Context) {

	fmt.Println("GetBook")

	id := c.Param("id")
	b := &Book{}

	res := b.query(id)

	fmt.Println(res)
	jsonBytes, err := json.Marshal(res)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(jsonBytes))

	c.JSON(200, gin.H{
		"message": string(jsonBytes),
	})
}

func View(c *gin.Context) {
	c.HTML(http.StatusOK, "test_index.html", gin.H{})
}

router.go 的源码如下。

package controllers

import (
	"github.com/gin-gonic/gin"
)

func Router(e *gin.Engine) {
	mainGroup := e.Group("/book")
	{
		mainGroup.GET("/get/:id", GetBook)
		mainGroup.GET("/hello", View)
	}
}

routers.go 的源码如下。

package routers

import (
	"github.com/gin-gonic/gin"
)

type Option func(*gin.Engine)

var options = []Option{}

func Include(opts ...Option) {
	options = append(options, opts...)
}

func Init() *gin.Engine {
	r := gin.New()

	for _, opt := range options {
		opt(r)
	}

	r.LoadHTMLGlob("views/*")

	return r
}

db.go 的源码如下。

package utils

import (
	"database/sql"
	"log"
)

func GetMySQLConnection() *sql.DB {

	Db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/tx")

	if err != nil {
		log.Panicln("err:", err.Error())
	}

	Db.SetMaxOpenConns(10)
	Db.SetMaxIdleConns(10)

	return Db
}

test_index.html 文件的源码如下。

<html>
    <head></head>
    <body>
        <h1>hello Go Web!</h1>
    </body>
</html>

index.go 文件的源码如下。

package main

import (
	"fmt"
	"gin/routers"

	"gin/controllers"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	routers.Include(controllers.Router)

	// 初始化所有的app的路由
	r := routers.Init()

	if err := r.Run(); err != nil {
		fmt.Println("startup service failed, err:%v\\n", err)
	}

	r.Run()
}
本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
【云+社区年度征文】我是如何 3 小时上手 Golang 的
Golang 是一门诞生 10 来年左右的“新”的编程语言(2009 年开源,相比 C 和 Java 是新语言),但是很多人推荐学习它,因为它的并发编程很方便,...
<<上一篇
下一篇>>