写在前面
本文是Go语言的快速入门教程,适合于具有一定C语言或者Java语言基础的开发人员,如果您是一位Go语言的熟练使用者,请绕行!像作者这个年纪的老码农通常都是从Pascal语言或者c语言开始学习计算机技术的,所以学起Go语言生来就有一种似曾相识的感觉,还是蛮轻松的!
当然,目前笔者也只能算是了解Go语言而已,还不能算熟悉,离精通那就差的更远了!说到这个计算机相关技术的掌握程度,又得发点牢骚,了解,熟悉,精通,这是不同的水平层次的表现。作为一个保守的,略带谦虚气质的老码农,我从来不敢说自己精通某某技术,某某语言,顶多敢在简历里面写上熟练掌握和使用某某语言、技术而已。如若让我看到谁的简历里面敢写精通某某语言、某某技术,碰巧又遇到我来面试,那他肯定死的很惨。我会面的他无地自容、信心丧失、立马寻死!当然大神除外,大神级的人物也轮不到我来面试,呵呵!
一、Go语言简介
咱们书归正传,来一起学习一下Go语言。Go的全称是GoLang,“够浪”你就来学Go语言,呵呵!笔者之前没有Go语言的任何经验,也是临危受命,被逼无奈,开始了“够浪”的学习和开发历程。
这个小地鼠不一般,它是Go语言的吉祥物,蛮可爱的!就和Go一样,简洁明了。
Go是一个开源的编程语言,它能够构造简单、可靠且高效的软件。Go是从2007年末由Google公司的Robert Griesemer, Rob Pike, Ken Thompson主持开发的,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
Go语言有如下特点:
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
下面我们就一起来开启“够浪”的快速学习之旅。
二、Go语言基础
计算机软件经历了数十年的发展,形成了多种学术流派,有面向过程编程、面向对象编程、函数式编程、面向消息编程等,这些思想究竟孰优孰劣,众说纷纭,作者不予评价。
Go语言对这些思想均有所吸收。例如,Go语言接受了函数式编程的一些想法,支持匿名函数与闭包。再如,Go语言接受了以Erlang语言为代表的面向消息编程思想,支持goroutine和通道,并推荐使用消息而不是共享内存来进行并发编程。总体来说,Go语言是一个非常现代化的语言,精小但非常强大。
Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。
Go 语言的主要特性如下:
- 自动垃圾回收
- 更丰富的内置类型
- 函数多返回值
- 错误处理
- 匿名函数和闭包
- 类型和接口
- 并发编程
- 反射
- 语言交互性
说了这么多,赶紧让大家来亲身体会一下吧!因为Go语言在语法上很大程度的类似C语言,故此本文不会面面俱到的讲解Go的语法,只会着重指出与C不同的地方以及Go自身有特色的内容。
第一个Go程序,仍然从hello world开始!
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
也像Java也像C,有package来做包管理,import来导入类库(包),main是入口函数。
- 第一行代码 package main 定义了包名。package main表示一个可独立执行的程序,每个Go应用程序都包含一个名为main的包;
- import "fmt" 告诉Go编译器这个程序需要使用fmt包;
- func main() 是程序开始执行的函数,即入口函数;
- 当标识符(包括常量、变量、类型、函数名、结构字段等)以一个大写字母开头,那么使用这种形式的标识符对象就可以被外部包的代码所使用,这被称为导出(类似OOP中的public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(类似于OOP中的protected )。
在Go程序中,一行代表一个语句结束。每个语句不需要以分号;结尾,因为这些工作都将由Go编译器自动完成。如果你打算将多个语句写在同一行,它们则必须使用;人为区分,但在实际开发中并不鼓励这种做法。
下面就从Go的语法方面介绍Go的特点,主要集中在区别于C语言的部分。
标识符
Go语言标识符、关键字、保留字、注释与C语言类似,不再赘述。
变量声明
Go使用var来声明变量,变量类型放在后面,例如:
var age int
一次可以声明多个变量,例如:
var a, b int = 1, 2
声明变量可以省略var,使用:=声明符(初始化声明)。但要注意省略 var, := 左侧如果没有声明新的变量,就产生编译错误。例如:
var intValue int
intValue := 1 //这时候会产生编译错误
intValue, intValue1 := 1, 2 //此时不会产生编译错误,因为有新的变量声明, := 是一个声明语句
可以将
var f string = "test"
简写为
f := "test"
因为Go可以根据值自行判定变量类型,可以在变量初始化时省略变量的类型而由系统自动推断。
指针
与c语言类似,Go也使用指针,&符号返回变量的地址,*符号声明指针变量。
条件判断语句
Go的条件判断语句同样有if else和switch,if语句没有什么特别之处,switch语句倒是有很大不同。
Go 编程语言中 if 语句的语法如下:
if 布尔表达式 {
}
if 布尔表达式 {
} else {
}
Go 编程语言中 switch 语句的语法如下:
switch var1 {
case val1:
...
case val2:
...
default:
...
}
变量var1可以是任何类型,而val1和val2则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。也可以同时测试多个可能符合条件的值,使用逗号分割。不同的case之间不需要使用break分隔。
switch语句还有一个fallthrough特性。使用fallthrough会强制执行后面的一条case语句,fallthrough不会判断下一条 case的表达式结果是否为true,直接执行。
Go语言还有一个特殊的控制结构,select语句,与switch语句有些类似。每个case 必须是一个通信操作,要么是发送要么是接收。select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
Go 编程语言中 select 语句的语法如下:
select {
case 通信操作 :
执行语句;
case 通信操作 :
执行语句;
...
default :
执行语句;
}
Range
在讲循环语句之前先说一下Range关键字。Go语言中range关键字用于for循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回key-value对的key值。
循环语句
Go 语言没有while和do...while语法,可以通过for 循环来实现其使用效果。Go语言的For循环有3种形式:
- 和 C 语言的 for 一样:for init; condition; post { }
- 和 C 语言的 while 一样:for condition { }
- 和 C 语言的 for(;;) 一样:for { }
循环控制语句有break, continue, goto。goto语句通常与条件语句配合使用。可用来实现条件转移,构成循环,跳出循环体等功能。但是,在结构化程序设计中一般不主张使用goto语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。
for循环的range格式可以对slice、map、数组、字符串等进行迭代循环。
for key, value := range aMap {
newMap[key] = value
}
Go的函数
Go语言函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
Go函数可以有多个返回值,这是Go的一大特点。如下例所示:
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b)
}
Go与C一样,参数传递方式有值传递和引用传递。默认情况下,Go语言使用的是值传递,即在调用过程中不会影响到实际参数。
Go的函数定义后可作为另外一个函数的实参数传入,Go语言可以很灵活的创建函数,并作为另外一个函数的实参。函数作为参数传递,实现回调,如下例所示:
package main
import "fmt"
// 声明一个函数类型
type cb func(int) int
func main() {
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("call back,x:%d\n", x)
return x
})
}
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("call back,x:%d\n", x)
return x
}
Go语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必声明。
Go语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type] {
/* 方法体 */
}
变量作用域
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为局部变量;
- 在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用;
- 函数定义中的变量称为形式参数;
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。
数组
Go语言数组声明需要指定元素类型及元素个数,语法格式如下:
var variable_name [SIZE] variable_type
初始化数组:
var balance = [6]float32{100.0, 2.1, 3.4, 7.6, 30.2, 8.9}
切片
Go语言切片实际上就是动态数组。与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
你可以声明一个未指定大小的数组来定义切片:
var sliceA []type
切片不需要说明长度。或使用make()函数来创建切片。也可以指定容量,其中capacity为可选参数。
make([]T, length, capacity)
切片提供了计算容量的方法cap()可以测量切片最长可以达到多少。一个切片在未初始化之前默认为nil,长度为0。
空值
Go语言的空值为nil,在概念上和其它语言的null、None、NULL一样。
结构体
Go语言的结构体与C语言类似,结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体有中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}
如果要访问结构体成员,需要使用点号"."操作符,使用结构体指针访问结构体成员,同样使用 "." 操作符。
Map
Key/value的键值对,可以使用内建函数 make ,也可以使用map关键字来定义Map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go语言类型转换基本格式如下:
type_name(expression)
type_name为类型,expression为表达式。
Interface
Go语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
type interface_name interface {
method1 [return_type]
method2 [return_type]
method3 [return_type]
...
methodN [return_type]
}
错误处理
Go语言通过内置的错误接口提供了非常简单的错误处理机制。error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
我们可以在编码中通过实现error接口类型来生成错误信息。函数通常在最后的返回值中返回错误信息,使用errors.New 可返回一个错误信息。
此外,panic与recover是Go的两个内置函数,这两个内置函数用于处理Go运行时的错误,panic用于主动抛出错误,recover用来捕获panic抛出的错误。还有一个defer语句,其目的类似于Java的finally,在当前函数的末尾执行一些清理代码,而不管此函数如何退出。defer的有趣之处在于它跟代码块没有联系,可以随时出现。
并发
Go语言支持并发,我们只需要通过go关键字来开启goroutine即可。goroutine是轻量级线程,goroutine的调度是由Golang运行时进行管理的。goroutine语法格式:
go 函数名( 参数列表 )
同一个程序中的所有goroutine共享同一个地址空间。goroutine是golang中在语言级别实现的轻量级线程,仅仅利用go就能立刻起一个新线程。
通道
通道(channel)是用来传递数据的一个数据结构。通道可用于两个goroutine之间通过传递一个指定类型的值来同步运行和通讯。操作符<- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把v发送到通道ch
v := <-ch // 从ch接收数据, 并把值赋给v
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建。
ch := make(chan int)
三、Web框架
互联网时代就得学习一下Web框架。Gin是一个go写的web框架,具有高性能的优点。官方地址:https://github.com/gin-gonic/gin。
1、下载并安装
$ go get -u github.com/gin-gonic/gin
2、在代码中导入它
import "github.com/gin-gonic/gin"
使用gin需要Go的版本号为1.6或更高。
下面我们就通过一个简单的例子来快速入门Gin。
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
router.Run() // listening on 0.0.0.0:8080
}
运行这段代码并在浏览器中访问 http://localhost:8080。
HTTP方法
可以使用 GET, POST, PUT, PATCH, DELETE, OPTIONS等方法。
参数获取
获取路径中的参数:
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
获取Get参数:
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname")
获取Post参数:
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")
路由分组
使用Group方法进行路由分组:
v1 := router.Group("/v1")
v2 := router.Group("/v2")
使用中间件
可以使用在全局上,也可以使用在分组上。
模型绑定和验证
若要将请求主体绑定到结构体中,请使用模型绑定,目前支持JSON、XML、YAML和标准表单值(foo=bar&boo=baz)的绑定。需要在绑定的字段上设置tag,比如,绑定格式为json,需要这样设置 json:"fieldname" 。当我们使用绑定方法时,Gin会根据Content-Type推断出使用哪种绑定器,如果你确定你绑定的是什么,你可以使用MustBindWith或者BindingWith。下面以绑定JSON类型为例:
// 绑定为json
type Login struct {
User string `json:"user" binding:"required"`
Password string `json:"password" binding:"required"`
}
func main() {
router := gin.Default()
// Example for binding JSON ({"user": "test", "password": "123"})
router.POST("/login", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "test" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "Login successfully!"})
})
}
关于Gin就讲这么多,够用了,想了解更加详细的内容,请访问Gin的官方网站。
四、ORM工具
有了Web框架,还得有ORM才算配齐。xorm是一个Go语言ORM库. 通过它可以使数据库操作非常简便。xorm的特性如下:
- 支持Struct和数据库表之间的灵活映射,并支持自动同步表结构
- 事务支持
- 支持原始SQL语句和ORM操作的混合执行
- 使用连写来简化调用
- 支持使用Id, In, Where, Limit, Join, Having, Table, Sql, Cols等函数和结构体等方式作为条件
- 支持级联加载Struct
- 支持LRU缓存(支持memory, memcache, leveldb, redis缓存Store) 和 Redis缓存
- 支持反转,即根据数据库自动生成xorm的结构体
- 支持事件
- 支持created, updated, deleted和version记录版本(即乐观锁)
下面还是通过一个简单的例子来入门xorm,使用Mysql数据库。
//匿名导入包:只导入包,而不使用任何包内的结构和类型,也不调用包内的任何函数
import _ "github.com/go-sql-driver/mysql"
type Account struct {
Id int64
Name string `xorm:"unique"`
Balance float64
Version int `xorm:"version"`
}
var x *xorm.Engine
x, err := xorm.NewEngine("mysql", "root:123456@/admin?charset=utf8")
获取数据
查询单条数据使用Get方法,在调用Get方法时需要传入一个对应结构体的指针,同时结构体中的非空field自动成为查询的条件和前面的方法条件组合在一起查询。
根据Id来获得单条数据:
a:=&Account{}
has, err := x.Id(id).Get(a)
根据where获取单条数据:
a := new(Account)
has, err := x.Where("name=?", "admin").Get(a)
返回的结果为两个参数,一个has(bool类型)为该条记录是否存在,第二个参数err为是否有错误。不管err是否为nil,has都有可能为true或者false。
批量获取数据
err = x.Desc("balance").Find(&as)
Find方法的第一个参数为slice的指针或Map指针,即为查询后返回的结果,第二个参数可选,为查询的条件struct的指针。
增删改操作
增加操作:
_, err := x.Insert(&Account{Name: name, Balance: balance})
删除操作:
_, err := x.Delete(&Account{Id: id})
方法 Delete 接受参数后,会自动根据传进去的值进行查找,然后删除。比如此处,我们指定了 Account 的 ID 字段,那么就会删除 ID 字段值与我们所赋值相同的记录;如果您只对 Name 字段赋值,那么 xorm 就会去查找 Name 字段值匹配的记录。如果多个字段同时赋值,
则是多个条件同时满足的记录才会被删除。
更新操作:
a := &Account{Id:1}
has, err := x.Get(a)
a.Balance += 1
_, err := x.Update(a)
事务
// 创建 Session 对象
sess := x.NewSession()
defer sess.Close()// 开启事务
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Update(a1); err != nil { // 发生错误时进行回滚
sess.Rollback()
return err
}
// 完成事务
return sess.Commit()
五、总结
实际上笔者也是一名Go的新手,该部分记录一下笔者在使用Go的过程中的一些经验总结以及踩过的坑。
- {不能单独放在一行;
- Go语言的字符串连接可以通过+实现;
- 空标识符“_”是一个占位符,用于在赋值操作的时候将某个值赋值给空标识符,从而达到丢弃该值的目的。空标识符不是一个新的变量,因此将它用于:=操作符的时候,必须同时为至少另一个值赋值;
- 如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明:=,编译器会提示错误 no new variables on left side of :=,但是=赋值是可以的,因为这是给相同的变量赋予一个新的值;
- 如果你在定义变量a之前使用它,则会得到编译错误 undefined: a;
- 如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误:xx declared and not used;
- 单纯地给变量(如:a)赋值是不够的,这个值必须被使用,但是全局变量是允许声明但不使用的;
- Go没有三目运算符,所以不支持 ?: 形式的条件判断;
- int转换为String类型时,不能用String()进行类型转换,而应该使用 strconv.Itoa();
Package strconv implements conversions to and from string representations of basic data types.
Itoa is equivalent to FormatInt(int64(i), 10)
如果使用string()的话,将返回UTF-8编码值。
string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text.
A string may be empty, but not nil. Values of string type are immutable.
六、学习资源
- 菜鸟教程 - https://www.runoob.com/go/go-tutorial.html
- Gin框架中文文档 - https://www.jianshu.com/p/98965b3ff638/
- golang-xorm库快速学习 - https://www.imooc.com/article/46419
2019年8月13日 星期二 于青岛