实战系列:(四)一天搞定Go语言

写在前面

       本文是Go语言的快速入门教程,适合于具有一定C语言或者Java语言基础的开发人员,如果您是一位Go语言的熟练使用者,请绕行!像作者这个年纪的老码农通常都是从Pascal语言或者c语言开始学习计算机技术的,所以学起Go语言生来就有一种似曾相识的感觉,还是蛮轻松的!
       当然,目前笔者也只能算是了解Go语言而已,还不能算熟悉,离精通那就差的更远了!说到这个计算机相关技术的掌握程度,又得发点牢骚,了解,熟悉,精通,这是不同的水平层次的表现。作为一个保守的,略带谦虚气质的老码农,我从来不敢说自己精通某某技术,某某语言,顶多敢在简历里面写上熟练掌握和使用某某语言、技术而已。如若让我看到谁的简历里面敢写精通某某语言、某某技术,碰巧又遇到我来面试,那他肯定死的很惨。我会面的他无地自容、信心丧失、立马寻死!当然大神除外,大神级的人物也轮不到我来面试,呵呵!

一、Go语言简介

       咱们书归正传,来一起学习一下Go语言。Go的全称是GoLang,“够浪”你就来学Go语言,呵呵!笔者之前没有Go语言的任何经验,也是临危受命,被逼无奈,开始了“够浪”的学习和开发历程。


地鼠logo

       这个小地鼠不一般,它是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是入口函数。

  1. 第一行代码 package main 定义了包名。package main表示一个可独立执行的程序,每个Go应用程序都包含一个名为main的包;
  2. import "fmt" 告诉Go编译器这个程序需要使用fmt包;
  3. func main() 是程序开始执行的函数,即入口函数;
  4. 当标识符(包括常量、变量、类型、函数名、结构字段等)以一个大写字母开头,那么使用这种形式的标识符对象就可以被外部包的代码所使用,这被称为导出(类似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的过程中的一些经验总结以及踩过的坑。

  1. {不能单独放在一行;
  2. Go语言的字符串连接可以通过+实现;
  3. 空标识符“_”是一个占位符,用于在赋值操作的时候将某个值赋值给空标识符,从而达到丢弃该值的目的。空标识符不是一个新的变量,因此将它用于:=操作符的时候,必须同时为至少另一个值赋值;
  4. 如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明:=,编译器会提示错误 no new variables on left side of :=,但是=赋值是可以的,因为这是给相同的变量赋予一个新的值;
  5. 如果你在定义变量a之前使用它,则会得到编译错误 undefined: a;
  6. 如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误:xx declared and not used;
  7. 单纯地给变量(如:a)赋值是不够的,这个值必须被使用,但是全局变量是允许声明但不使用的;
  8. Go没有三目运算符,所以不支持 ?: 形式的条件判断;
  9. 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.

六、学习资源

  1. 菜鸟教程 - https://www.runoob.com/go/go-tutorial.html
  2. Gin框架中文文档 - https://www.jianshu.com/p/98965b3ff638/
  3. golang-xorm库快速学习 - https://www.imooc.com/article/46419

                                                                             2019年8月13日 星期二 于青岛

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342