阅读 1630

Go 快速入门指南 | 🏆 技术专题第二期征文

前言

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,垃圾回收,结构形态及 CSP-style 并发计算。

温馨提示:由于本文偏基础,所以这里适用对象包括:对 Go 感兴趣的前端开发工程师和没有接触过这门语言的JAVA开发工程师。这是一门非常年轻的语言,希望大家能通过本文了解 Go 的基本玩法。如果还能玩的开心那就再好不过了。

初识 Go 语言

特色

  • 简洁、快速、安全
  • 并行、有趣、开源
  • 内存管理、数组安全、编译迅速

用途

  • 服务器编程
  • 分布式系统、数据库代理器、中间件
  • 网络编程,Web应用、API应用、下载应用
  • 数据库操作
  • 开发云平台

环境安装

支持的系统:

  • Linux
  • FreeBSD
  • Mac OS
  • Windows

Mac 系统下安装

  1. 下载二进制包:go1.15.darwin-amd64.tar.gz
  2. 将下载的二进制包解压至 /usr/local 目录
tar -C /usr/local -xzf go1.15.darwin-amd64.tar.gz
复制代码
  1. /usr/local/go/bin 目录添加至 PATH 环境变量:
export PATH=$PATH:/usr/local/go/bin
复制代码

温馨提示:MAC 系统下你可以使用 .pkg 结尾的安装包直接双击来完成安装,安装目录在 /usr/local/go/

Windows 系统下安装

然后说说 Windows 平台,Windows 系统可以直接下载 go1.15.windows-amd64.msi 安装包来安装。(安装包窗口,连续点击下一步即可)

默认情况下,安装文件会安装在 C:\Go 目录下。

新建 test.go 文件,输入以下示例代码:

/* test.go */
package main
import "fmt"
func main()  {
	fmt.Println("Hello, World!")
}
复制代码

命令行执行代码输出结果如下:

Microsoft Windows [版本 10.0.18363.959]
(c) 2019 Microsoft Corporation。保留所有权利。

D:\Workspace>go run test.go
Hello, World!
复制代码

语言结构

Go 语言的基础组成:

  • 包声明
  • 引入包
  • 函数
  • 变量
  • 语句 & 表达式
  • 注释

看下 test.go 的各个部分:

  1. package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main
  2. import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
  3. func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
  4. fmt.Println("Hello, World!") 可以将字符串输出到控制台。
  5. 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

数据类型

类型描述
布尔型布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
数字类型整型 int 和浮点型 float32float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
字符串类型字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
派生类型包括:指针类型(Pointer)、数组类型、结构化类型(struct)、Channel 类型、函数类型、切片类型、接口类型(interface)、Map 类型

变量

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。

声明变量的一般形式是使用 var 关键字。

变量声明

指定变量类型,如果没有初始化,则变量默认为零值。

package main
import "fmt"
func main() {

    /* 声明一个变量并初始化 */
    var a = "hello"
    fmt.Println(a) /* hello */

    /* 没有初始化就为零值 */
    var b int
    fmt.Println(b) /* 0 */

    /* bool 零值为 false */
    var c bool
    fmt.Println(c) /* false */
}
复制代码

也可以根据值自行判定变量类型。

package main
import "fmt"
func main() {
    var d = true
    fmt.Println(d) /* true */
}
复制代码

还有一种声明方式,省略 var, 注意 := 左侧如果没有声明新的变量,就产生编译错误,格式:

package main
import "fmt"
func main() {
    f := "hello" /* var f string = "hello" */
    fmt.Println(f)
}
复制代码

注意:当变量类型为引用类型,且存在赋值操作(例如:a = b)时,只有引用(地址)被复制。

指针

变量是一种使用方便的占位符,用于引用计算机内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

package main
import "fmt"
func main()  {
    var a int = 100  
    fmt.Printf("变量的内存地址: %x\n", &a  )
}

/* 输出 */
变量的内存地址: c000010080
复制代码

什么是指针

一个指针变量指向了一个值的内存地址。

var ip *int        /* 指向整型 */
var fp *float32    /* 指向浮点型 */
复制代码

如何使用指针

使用流程:

  • 定义指针变量
  • 为指针变量赋值
  • 访问指针变量中指向地址的值

在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

package main
import "fmt"
func main() {
   var a int = 10   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */

   ip = &a  /* 指针变量的存储地址 */

   fmt.Printf("a 变量的地址是: %x\n", &a  )

   /* 指针变量的存储地址 */
   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )

   /* 使用指针访问值 */
   fmt.Printf("*ip 变量的值: %d\n", *ip )
}

/* 输出 */
a 变量的地址是: c000010080
ip 变量储存的指针地址: c000010080
*ip 变量的值: 10
复制代码

空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil

nil 指针也称为空指针。

nil 在概念上和其它语言的 null、None、nil、NULL 一样,都指代零值或空值。

一个指针变量通常缩写为 ptr

结构体

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

定义结构体

结构体定义需要使用 typestruct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。

实例:

package main
import "fmt"
type Games struct {
   name string
   url string
   class string
   game_id int
}


func main() {

    // 创建一个新的结构体
    fmt.Println(Games{"英雄联盟", "https://lol.qq.com/main.shtml", "MOBA", 666666})

    // 也可以使用 key => value 格式
    fmt.Println(Games{name: "英雄联盟", url: "https://lol.qq.com/main.shtml", class: "MOBA", game_id: 666666})

    // 忽略的字段为 0 或 空
   fmt.Println(Games{name: "英雄联盟", url: "https://lol.qq.com/main.shtml"})
}

/* 输出 */
{英雄联盟 https://lol.qq.com/main.shtml MOBA 666666}
{英雄联盟 https://lol.qq.com/main.shtml MOBA 666666}
{英雄联盟 https://lol.qq.com/main.shtml  0}
复制代码

访问结构体成员

如果要访问结构体成员,需要使用点号 . 操作符,格式为:

结构体.成员名
复制代码

实例:

package main
import "fmt"
type Games struct {
   name string
   url string
   class string
   game_id int
}

func main() {
   var LOL Games /* 声明 LOL 为 Games 类型 */

   /* LOL 描述 */
   LOL.name = "英雄联盟"
   LOL.url = "https://lol.qq.com/main.shtml"
   LOL.class = "MOBA"
   LOL.game_id = 666666

   /* 打印 LOL 信息 */
   fmt.Printf( "LOL name : %s\n", LOL.name)
   fmt.Printf( "LOL url : %s\n", LOL.url)
   fmt.Printf( "LOL class : %s\n", LOL.class)
   fmt.Printf( "LOL game_id : %d\n", LOL.game_id)
}

/* 输出 */
LOL name : 英雄联盟
LOL url : https://lol.qq.com/main.shtml
LOL class : MOBA
LOL game_id : 666666
复制代码

结构体指针

如果想在函数里面改变结果体数据内容,需要传入指针:

package main
import "fmt"
type Games struct {
   name string
   url string
   class string
   game_id int
}

func changeGame(game *Games) {
    game.name = "云顶之弈"
}

func main() {
    var LOL Games
    LOL.name = "英雄联盟"
   	LOL.url = "https://lol.qq.com/main.shtml"
   	LOL.class = "MOBA"
   	LOL.game_id = 666666
    changeGame(&LOL)
    fmt.Println(LOL)
}

/* 输出 */
{云顶之弈 https://lol.qq.com/main.shtml MOBA 666666}
复制代码

切片( Slice )

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片

/* 声明一个未指定大小的数组来定义切片 */
var identifier []type

/* 使用 make() 函数来创建切片 */
var slice1 []type = make([]type, len)
/* 简写 */
slice1 := make([]type, len)

/* 也可以指定容量,其中 capacity 为可选参数 */
make([]T, length, capacity)
复制代码

这里 len 是数组的长度并且也是切片的初始长度。

切片初始化

s :=[] int {1, 2, 3} 
复制代码

直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3。这里cap=len=3

s := arr[:]
复制代码

初始化切片 s ,是数组 arr 的引用

s := arr[startIndex:endIndex] 
复制代码

arr 中从下标 startIndexendIndex-1 下的元素创建为一个新的切片

s := arr[startIndex:] 
复制代码

arr 中从下标 startIndex 到最后一个元素(默认 endIndex 情况)

s := arr[:endIndex] 
复制代码

将表示从 arr 的第一个元素开始(默认 startIndex 情况),到下标 endIndex 下的元素

s1 := s[startIndex:endIndex] 
复制代码

通过切片 s 初始化切片 s1

s :=make([]int, len, cap) 
复制代码

通过内置函数 make() 初始化切片 s[]int 标识为其元素类型为 int 的切片

len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

package main
import "fmt"
func main() {
   var numbers = make([]int, 3, 10)

   printSlice(numbers)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

/* 输出 */
len=3 cap=10 slice=[0 0 0]
复制代码

范围( Range )

Go 语言中 range 关键字用于 for 循环中迭代数组( array )、切片( slice )、通道( channel )或集合( map )的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

package main
import "fmt"
func main() {
    /* 这是我们使用 range 去求一个 slice 的和。使用数组跟这个很类似 */
    nums := []int{1, 2, 3}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)
    /* 在数组上使用 range 将传入 index 和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引 */
    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }
    /* range 也可以用在 map 的键值对上 */
    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }
    /* range 也可以用来枚举 Unicode 字符串。第一个参数是字符的索引,第二个是字符(Unicode 的值)本身 */
    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

/* 输出 */
sum: 6
index: 2
a -> apple
b -> banana
0 103
1 111
复制代码

Map (集合)

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

定义 Map

可以使用内建函数 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)
复制代码

如果不初始化 map,那么就会创建一个 nil mapnil map 不能用来存放键值对

下面实例演示了创建和使用 map:

package main
import "fmt"
func main() {
    var cnNameMap map[string]string /* 创建集合 */
    cnNameMap = make(map[string]string)

    /* map 插入 key-value 对,各个英文对应的中文名 */
    cnNameMap [ "Ashe" ] = "寒冰射手·艾希"
    cnNameMap [ "Annie" ] = "黑暗之女·安妮"
    cnNameMap [ "Alistar" ] = "牛头酋长·阿利斯塔"
    cnNameMap [ "Twisted" ] = "卡牌大师·崔斯特"

    /* 使用键输出中文名 */
    for cn := range cnNameMap {
        fmt.Println(cn, "中文名是", cnNameMap [cn])
    }

    /* 查看元素在集合中是否存在 */
    name, ok := cnNameMap [ "Sivir" ] /* 如果确定是真实的,则存在,否则不存在 */
    /* fmt.Println(name) */
    /* fmt.Println(ok) */
    if (ok) {
        fmt.Println("Sivir 的中文名是", name)
    } else {
        fmt.Println("Sivir 的中文名不存在")
    }
}

/* 输出 */
Annie 中文名是 黑暗之女·安妮
Alistar 中文名是 牛头酋长·阿利斯塔
Twisted 中文名是 卡牌大师·崔斯特
Ashe 中文名是 寒冰射手·艾希
Sivir 的中文名不存在
复制代码

delete() 函数

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key

实例如下:

package main
import "fmt"
func main() {
    /* 创建 map */
    cnNameMap := map[string]string{
    	"Annie": "黑暗之女·安妮", 
        "Alistar": "牛头酋长·阿利斯塔", 
        "Twisted": "卡牌大师·崔斯特",
        "Ashe": "寒冰射手·艾希",
    	"Sivir": "战争女神·希维尔",
    }

    fmt.Println("英雄联盟")

    /* 打印中文名 */
    for cn := range cnNameMap {
    	fmt.Println(cn, "中文名是", cnNameMap [ cn ])
    }

    /* 删除元素 */ delete(cnNameMap, "Alistar")
    fmt.Println("牛头酋长·阿利斯塔被删除")

    fmt.Println("删除元素后英雄联盟")

    /*打印中文名*/
    for cn := range cnNameMap {
    	fmt.Println(cn, "中文名是", cnNameMap [ cn ])
    }
}

/* 输出 */
英雄联盟
Annie 中文名是 黑暗之女·安妮
Alistar 中文名是 牛头酋长·阿利斯塔
Twisted 中文名是 卡牌大师·崔斯特
Ashe 中文名是 寒冰射手·艾希
Sivir 中文名是 战争女神·希维尔
牛头酋长·阿利斯塔被删除
删除元素后英雄联盟
Ashe 中文名是 寒冰射手·艾希
Sivir 中文名是 战争女神·希维尔
Annie 中文名是 黑暗之女·安妮
Twisted 中文名是 卡牌大师·崔斯特
复制代码

类型转换

类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go 语言类型转换基本格式如下:

类型(表达式)

以下实例中将整型转化为浮点型

package main
import "fmt"
func main() {
   var sum int = 10
   var count int = 3
   var mean float32
   
   mean = float32(sum)/float32(count)
   fmt.Printf("mean 的值为: %f\n",mean)
}

/* 输出 */
mean 的值为: 3.333333
复制代码

接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

package main
import (
    "fmt"
)

type Phone interface {
    call()
}

type AndroidPhone struct {
}

func (androidPhone AndroidPhone) call() {
    fmt.Println("I am Android, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(AndroidPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()
}

/* 输出 */
I am Android, I can call you!
I am iPhone, I can call you!
复制代码

例子中,我们定义了一个接口 Phone,接口里面有一个方法 call()。然后我们在 main 函数里面定义了一个 Phone 类型变量,并分别为之赋值为 AndroidPhoneIPhone

错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error 类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}
复制代码

函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
}
复制代码

实例:

package main
import "fmt"
/* 定义一个 DivideError 结构 */
type DivideError struct {
    dividee int
    divider int
}

/* 实现 `error` 接口 */
func (de *DivideError) Error() string {
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
    return fmt.Sprintf(strFormat, de.dividee)
}

/* 定义 `int` 类型除法运算的函数 */
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
        dData := DivideError{
            dividee: varDividee,
            divider: varDivider,
        }
        errorMsg = dData.Error()
        return
    } else {
        return varDividee / varDivider, ""
    }
}

func main() {
    /* 正常情况 */
    if result, errorMsg := Divide(100, 10); errorMsg == "" {
    	fmt.Println("100/10 = ", result)
    }
    /* 当除数为零的时候会返回错误信息 */
    if _, errorMsg := Divide(100, 0); errorMsg != "" {
    	fmt.Println("errorMsg is: ", errorMsg)
    }
}

/* 输出 */
100/10 =  10
errorMsg is:
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0
复制代码

并发

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

例如:

go f(a, b, c)

开启一个新的 goroutine:

f(a, b, c)

同一个程序中的所有 goroutine 共享同一个地址空间。

package main
import (
	"fmt"
	"time"
)
func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("hi")
	say("hello")
}

/* 输出 */
hi
hello
hello
hi
hello
hi
hello
hi
hello
复制代码

参考文章

感谢

  • 本文就 Go 语言作了初步介绍,希望能起到抛砖引玉的效果。
  • 文中如有错误,欢迎在评论区指正。
  • 如果本文对你有帮助,就点个赞支持下吧!感谢阅读。

🏆 技术专题第二期 | 我与 Go 的那些事......