# Go 快速入门(一)

# 前言

随着公司的业务发展,原先支撑起我们大部分业务的 PHP 技术栈已经不堪重负,所以我们逐渐引入了 Java 技术栈,帮我们解决了很多问题。

然而对于很多 PHP 程序员而言,Java 往往意味着替换而不是补充,转型之路又面临着陡峭的学习曲线,往往令人望而却步。

幸而,Golang 指出了另一条明路:上手简单、性能强大,而且可以跟 PHP 友好相处。

事实上,**业务就曾经在遇到高并发的业务时,用 Go 实现了部分低性能的 API ,有效地解决了业务压力;我们的**服务也通过用 Go 进行重写,替换了 PHP 版本,在减少了服务器资源的情况下,还极大地增加了系统的承载能力,有效地保障了上层业务的稳健运行。

在此,我邀请了负责咱们**服务的 Kaisco 同学,为大家准备这个 Golang 的快速入门系列,计划用每周一篇的节奏,带着大家在 1 ~ 2 个月的时间里,不仅掌握 Go 语言的基础使用,还能实现一个 上万 TPS 的简化版 Redis

感兴趣吗?那就快点参与进来,还有,别忘了完成课后习题。(By 冯少)

# 一、Go语言环境安装

# 1、 安装

根据操作系统到官网中国镜像下载安装包,并根据不同操作系统安装教程安装golang。注意区分自己的操作系统是32位还是64位。

# 2.、环境变量

# 2.1、GOROOT

Go1.10版本引入了默认GOROOT,即开发者无需显式设置GOROOT环境变量。唯一要做的就是将安装目录下bin目录路径放置到PATH环境变量中。

  • 类Unix系统
export PATH=$PATH:go-install-dir/bin
  • Windows
SETX "PATH" "%PATH%;go-install-dir/bin" /M

# 2.2、GOPATH

GOPATH环境变量指定工作区的位置,GOPATH可以是您系统上的任何目录,但是GOPATH该路径不能与Go安装相同

Go1.8版本引入默认的GOPATH。在Linux/Mac系下,默认的GOPATH为$HOME/go,在Windows下,GOPATH默认路径为:%USERPROFILE%/go。

GOPATH 下目录约定有三个子目录:

GOPATH
	- src 存放源代码(在该目录下进行开发)
	- pkg 编译后生成的文件
	- bin 编译后生成的可执行文件

GOPATH 中允许设置多个目录(但不常用),通常会将所有Go代码保存在一个GOPATH中。

此外,有些 IDE 可以给每个项目设立单独的 GOPATH ,避免项目之间的依赖包出现冲突(类似PHP的composer或者Python的virtualenv)。

# 3、Hello World

// $GOPATH/src/hello_world.go
package main

import "fmt"

func main() {
    fmt.Println("Hello world or 你好 世界 or 안녕 세상 or こんにちはせかい")
}

$GOPATH/src目录下输入命令go run hello_world.go运行程序。

程序输出:

Hello world or 你好 世界 or 안녕 세상 or こんにちはせかい

# 二、Go基础概念

# 1、关键字

breakdefaultfuncinterfaceselectcasedefergomapstructchanelsegotopackageswitchconstfallthroughifrangetypecontinueforimportreturnvar

# 2、包(package)

Go使用package来组织代码。package的基本作用:模块化(能够把程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。

每一个可独立运行的Go程序,必定包含一个package main,在这个main包中必定包含一个入口函数main,而这个函数既没有参数,也没有返回值。

# 3、import

Go代码的时候经常用到import这个命令用来导入包文件。

举例:

import "fmt"

或者

import (
  "fmt"
  "math"
)

# 4、可见性

当标识符(包括常量、变量、类型、函数名、结构字段等)以一个大写字母开头,那么使用这种形式的标识符可以被外部包的代码所使用,这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。

例如:在 package test1 中定义两个方法 Foo 和 bar,在 test2 中只能调用 test1.Foo,无法调用 test1.bar.

# 三、Go语言基础

# 1、数据类型

bool、rune、int8、 int16、int32、int64、byte、uint8、uint16、uint32、uint64、float32、float64、string、error(内置error类型,专门用来处理错误信息)等等基本数据类型。

# 2、变量、常量

# 2.1、变量

Go语言里面定义变量有多种方式。

var关键字是Go最基本的定义变量方式,Go把变量类型放在变量名后面:

//定义一个名称为“variableName”,类型为"type"的变量
var variableName type

// 定义变量并初始化值
var variableName type = value

//定义三个类型都是“type”的变量
var vname1,vname2,vname3 type

// 定义三个类型都是"type"的变量,并且分别初始化为相应的值vname1为v1,vname2为v2,vname3为v3
var vname1,vname2,vname3 type= v1,v2,v3

// 定义三个变量,它们分别初始化为相应的值vname1为v1,vname2为v2,vname3为v3。然后Go会根据其相应值的类型来决定变量的基本数据类型
var vname1,vname2,vname3 = v1,v2,v3
// 或者使用`简短声明`方式
vname1,vname2,vname3 := v1,v2,v3
  • 简短声明有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用var方式来定义全局变量。

  • Go对于已声明但未使用的变量会在编译阶段报错

  • _(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。

    _, b := 34, 35
    

# 2.2、常量

常量就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。

const constantName = value
//如果需要,也可以明确指定常量的类型:
const Pi float32 = 3.1415926

# 2.3、分组声明

在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。

import(
    "fmt"
    "os"
)

const(
    i = 100
    pi = 3.1415
    prefix = "Go_"
)

var(
    i int
    pi float32
    prefix string
)

# 3、slice、map

# 3.1、slice

  • 定义方式

    // 和声明array一样,只是少了长度
    var fslice []int
    // 初始化数据
    slice := []byte {'a', 'b', 'c', 'd'}
    
  • slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i

    // 声明一个含有10个元素元素类型为byte的数组
    var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
    
    // 声明两个含有byte的slice
    var a, b []byte
    
    // a指向数组的第3个元素开始,并到第五个元素结束,
    a = ar[2:5]
    //现在a含有的元素: ar[2]、ar[3]和ar[4]
    
    // b是数组ar的另一个slice
    b = ar[3:5]
    // b的元素是:ar[3]和ar[4]
    

    注意slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。

  • slice简便的操作

    • slice的默认开始位置是0,ar[:n]等价于ar[0:n]
    • slice的第二个序列默认是slice的长度,ar[n:]等价于ar[n:len(ar)]
    • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]
  • ``slice`有几个有用的内置函数

    • len 获取slice的长度
    • cap 获取slice的最大容量
    • appendslice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
    • copy 函数copy从源slicesrc中复制元素到目标dst,并且返回复制的元素的个数

# 3.2、map

map的格式为map[keyType]valueType

// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
var numbers map[string]int

// 另一种map的声明方式
numbers := make(map[string]int)
numbers["one"] = 1  //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3

fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据
// 打印出来如:第三个数字是: 3

// 通过delete删除map的元素
delete(numbers, "ten")  // 删除key为ten的元素

// 判断map是否存在某个元素
if _, ok := numbers["four"]; ok {
	// ok 为true即存在
}
  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
  • map的长度是不固定的,也就是和slice一样,也是一种引用类型
  • 内置的len函数同样适用于map,返回map拥有的key的数量
  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制

# 3.3、make、new操作

make用于内建类型(mapslicechannel)的内存分配。new用于各种类型的内存分配。

# 4、函数和流程

# 4.1、函数

函数使用关键字func来声明。

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //这里是处理逻辑代码
    //返回多个值
    return value1, value2
}
  • 关键字func用来声明一个函数funcName
  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔
  • 函数可以返回多个值
  • 上面返回值声明了两个变量output1output2,如果你不想声明也可以,直接就两个类型
  • 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
  • 如果没有返回值,那么就直接省略最后的返回信息
  • 如果有返回值, 那么必须在函数的外层添加return语句

Go函数支持变参。接受变参的函数是有着不定数量的参数的。

func myfunc(arg ...int) {}

defer

Go语言中有种不错的设计,即延迟(defer)语句,可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。

init函数

Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

Go程序会自动调用init()main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

# 4.2、流程

# 4.2.1、if

Go里面if条件判断语句中不需要括号。

// 用法一
if x > 10 {
	// ...
}

// 用法二
if x > 10 {
	// ...
} else {
	// ...
}

// 用法三
if x := computedValue(); x > 10 {
   // ...
} 

# 4.2.2、for

Go里面最强大的一个控制逻辑就是for,它即可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操作。它的语法如下:

for expression1; expression2; expression3 {
    // ...
}

for循环作为其它语言while语句:

for expression {
    // ...
}

for配合range可以用于读取slicemap的数据:

for k,v:=range map {
   // ...
}

# 本章试练

  • 安装Go环境,实现hello world并编译运行
  • 用牛顿法实现平方根函数 - LeetCode 题目地址
  • 实现一个fibonacci 函数,返回一个数列的迭代器(一个函数),每次调用这个迭代器函数,都会返回该数列的下一个元素。斐波纳契数列 (0, 1, 1, 2, 3, 5, ...) - 查看答案
  • 给定一个单词列表,只返回可以使用在键盘同一行的字母打印出来的单词LeetCode 题目地址
  • 给定一个整数,将其转化为7进制,并以字符串形式输出LeetCode 题目地址