Go courses

  1. Go 基础教程
  2. Go Exercises | Learn Go Programming
  3. Exercism
  4. Courses Dashboard | courses.calhoun.io

1. 什么是 Go?

Go 是一门 并发支持 、垃圾回收 的 编译型 系统编程语言,旨在创造一门具有在静态编译语言的高性能和动态语言的高效开发之间拥有良好平衡点的一门编程语言。

Go 的主要特点有哪些?

  • 类型安全内存安全
  • 以非常直观极低代价的方案实现 高并发
  • 高效的垃圾回收机制
  • 快速编译(同时解决 C 语言中头文件太多的问题)
  • 为多核计算机提供性能提升的方案
  • UTF-8 编码支持

Go 环境变量与工作目录

根据约定,GOPATH 下需要建立 3 个目录:

  • bin(存放编译后生成的可执行文件)
  • pkg(存放编译后生成的包文件)
  • src(存放项目源码)

Go 常用命令简介

  • go get:获取远程包(需提前安装 git 或 hg)
  • go run:直接运行程序(直接编译生成临时文件,直接开始运行)
  • go build:测试编译,检查是否有编译错误( package 名称为:main 的才可以使用它,然后生成可执行文件)
  • go fmt:格式化源码(部分 IDE 在保存时自动调用)
  • go install:编译包文件并编译整个程序(编译包文件 -> 生成“操作系统名_cpu 型号” 的文件夹 -> 在该文件夹下的 bin 文件夹里生成可执行文件 -> 当你要运行时,需要把可执行文件拷到 bin 文件夹同级别, 因为是相对路径)
  • go test:运行测试文件(以 “*_test.go” 结尾的文件)
  • go doc:查看文档

1.1. 简单例子

1
2
3
4
5
6
7
8
package main

import "fmt"

func main(){
fmt.Println("Hello World")
}

go build 会编译名为 main 的 package 包,然后我们导入管理格式化输入输出的 fmt 包。 func main(){} 是函数,而且 main 函数没有参数以及返回值, 左边 { 需要和 main() 同行,因为 go 虽然不想 c 语言显式要求以分号结尾,但是编译器会自动加上, 如果把 左 { 下载下一行,那么就会报错。

1.2. 格式化输入

fmt.Print vs fmt.Println vs fmt.Printf

fmt.Print and fmt.Println print the raw string (fmt.Println appends a newline)

fmt.Printf will not print a new line, you will have to add that to the end yourself with \n.

The way fmt.Printf works is simple, you supply a string that contains certain symbols, and the other arguments replace those symbols. For example:

参考:Println vs Printf vs Print in Go - Stack Overflow

2. Go 程序结构

2.1. 基本结构

  • Go 程序是通过 package 来组织的
  • 只有 package 名称为main 的包可以包含 main 函数
  • 一个可执行程序 有且仅有 一个 main 包

2.2. 关键字标识符

  • 通过 import 关键字来导入其它非 main 包
  • 通过const 关键字来进行常量的定义
  • 通过在函数体外部使用 var 关键字来进行全局变量的声明与赋值
  • 通过 type 关键字来进行结构(struct)或接口(interface)的声明
  • 通过func 关键字来进行函数的声明 Go 导入 package 的格式:
1
2
3
4
5
import(
"fmt"
"math"
"time"
)

导入包之后,就可以使用格式 . 来对包中的函数进行调用 如果导入包之后 未调用 其中的函数或者类型将会报出编译错误:imported and not used

2.3. Package 别名

当使用第三方包时,包名可能会非常接近或者相同,此时就可以使用别名来进行区别和调用。 例如:

1
2
3
4
5
6
7
...
import(
io "fmt"
)
...
io.Println("Hello World")
...

2.3.1. 省略调用

相当于把包别名定义成 . 当前目录,然后就可以直接使用包中的函数了。

1
2
3
4
import(
. "fmt"
)

  • 不建议使用,易混淆
  • 不可以和别名同时使用

2.4. 可见性规则

Go 语言中,使用 大小写 来决定该 常量、变量、类型、接口、结构 或函数 是否可以被外部包所调用:

根据约定,函数名首字母小写即为 private,函数名首字母大写即为 public. 这样,当你调用其他包的函数时,函数开头一定为大写,否则就写错了。

简单 go 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 当前程序包名, package main 需要下载非注释的第一行
package main

//导入其他包名
import (
"fmt"
"math/rand"
)

//常量的定义
const (
PI = 3.14
const1 = 1
)

//全局变量的声明与赋值
var (
name = "gopher"
id = 3
)

// 一般类型声明
type (
newType int
type1 float32
type2 string
type3 byte
)

//结构声明
type gopher struct{}

//接口声明

type golang interface{}

//由 main 函数作为程序入口点启动
func main() {
fmt.Println("Hello World" + rand.Intn(19))
}

导入多个包时可以进行简写,声明多 常量、全局变量或一般类型(非接口、非结构)也可以用同样的方法, 我们称之为"常量组, 全局变量组,一般类型组" 注意:var 声明全局变量的方法,不适合在函数中声明变量使用,只适合全局变量

3. Go 基本类型

类型

  1. 布尔型 布尔型 bool - 长度:1 字节 - 取值范围:true。false - 注意事项:不可以用数字代替 true 或 false,也不能和整形进行强制类型转换,二者是不兼容的类型 2.整形 int/uint - 根据运行平台可能为 32 或 64 位

    8 位整型:`int8/uint8`
    - 长度:1字节
    - 取值范围:-128~127/0~255
    
    字节型:`byte`(`uint8` 的别名`)
        为什么要有别名的?为了更好地区分应用场景,增强代码可读性。
        比如:这里当你想要进行字节相关的操作时, 显然用 byte 更加具体,可读。
    
    16位整型:`int16/uint16`
        - 长度:2字节
        - 取值范围:-32768~32767/0~65535
    32位整型:`int32(rune)/uint32`
        - 长度:4字节
        - 取值范围:-2^32/2~2^32/2-1/0~2^32-1
        - rune 当场景与 unicode 有关时。
    64位整型:`int64/uint64`
        - 长度:8字节
        - 取值范围:-2^64/2~2^64/2-1/0~2^64-1
  2. 浮点型:float32/float64

    • 长度:4/8 字节
    • 小数位:精确到 7/15 小数位
  3. 复数:complex64/complex128

    • 长度:8/16 字节 足够保存指针的 32 位或 64 位整数型:uintptr
  4. 其它值类型:

    • array、struct、string
  5. 引用类型:

    • slice、map、chan slice 切片, map 类似 hash table, chan 通道,处理高并发通信
  6. 接口类型:inteface

  7. 函数类型:func

  8. 指针 Go 虽然保留了指针,但与其它编程语言不同的是,在** Go 当中不支持指针运算以及 ”->” 运算符,而直接采用 ”.” 选择符来操作指针目标对象的成员**

操作符 ”&” 取变量地址,使用 ”*” 通过指针间接访问目标对象 默认值为 nil 而非 NULL

递增递减语句 在 Go 当中,++ 与 -- 是作为语句而并不是作为表达式, 因此,需要将 a++, a-- 当作单独的语句来写,而不能放在等号右边,当作右值。 定义:

1
2
3
a := 1
var p *int = &a
fmt.PrintlnI(*p)

补充: math 包中有 Max 和 Min 开头的标识类型最大最小的值:

1
2
3
4
5
6
7
8
9

import (
"math"
)
....
fmt.Println(math.MaxUint32)

OUTPUT:
4294967295

类型零值

零值并不等于空值,而是当变量被声明为某种类型后的默认值, 通常情况下值类型的默认值为 0,bool 为 false,string 为空字符串

类型别名

1
2
3
4
5
6
type (
文本 string
)
..
var chinese 文本 = "中文"
fmt.Println(chinese)

从严格意义上讲, type newInt int, 这里 newInt 并不能说是 int 的别名,而只是底层数据结构相同,在这里称为自定义类型,在进行类型转换时仍旧需要显式转换, 但 byte 和 rune 确确实实为 uint8 和 int32 的别名,可以相互进行转换。

单个变量的声明与赋值

  • 变量的声明格式:var <变量名称><变量类型>
  • 变量的赋值格式:<变量名称>=<表达式>
  • 声明的同时赋值:var <变量名称>[变量类型]=<表达式>
  • 声明的同时赋值最简形式:<变量名称> := <表达式> go 编译器会根据赋的值进行类型推断, 而 : 相当于 var

声明与赋值分开的形式应用场景: 当你需要在分支结构中使用,但是在结束分支分支结构后,还需要使用它,那么就需要提前在外面声明。

1
2
3
4
5
6
7
8
9
10
11
func main() {
var a int
a = 2 //a = 1.1 是错误的,不能隐式转换
var b int = 3
var c = 4 //因为 go编译器可以进行类型推断,因此声明同时赋值时,可以省略类型。
d := false //声明同时赋值 的最简形式, go 编译器会根据赋的值进行类型推断
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}

多个变量的声明与赋值

  • 全局变量的声明可使用 var() 的方式进行简写
  • 全局变量的声明不可以省略 var,但可使用并行方式
  • 所有变量都可以使用类型推断
  • var() 变量组可以在函数体内用于声明局部变量,感谢 Hubery 指出,但并不建议这样使用。

补充:使用空白符号 _ 对赋值进行忽略,在比如:接受函数多个返回值的场景及其有用

1
2
3
4
5
6
7
8
9
10
11
12
13
var (
var1, var2 = 5, 6
)

func main() {
a, _, c, d := 1, 2, 3, 4
fmt.Println(a)
fmt.Println(c)
fmt.Println(d)
fmt.Println(d)
fmt.Println(var1)
fmt.Println(var2)
}

变量的类型转换

  • Go 中不存在隐式转换,所有类型转换必须显式声明
  • 转换只能发生在两种相互兼容的类型之间(注: 布尔类型与整型不兼容)
  • 类型转换的格式: [:]= ()
1
2
3
4
5
6
func main() {
a := 1.2
var b int = int(a)
fmt.Println(a)
fmt.Println(b)
}

如果将数字转换为 string 会对应为 ascii 码中的字符, 如果只想数字转换为字符串类型的数字,可以使用 "strconv" 包 中的 Itoa 函数(当然, 也有 Atoi 起相反作用。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"math"
"reflect"
"strconv"
)
...

var a int = 65
b := string(a)
c := strconv.Itoa(a)
fmt.Println(reflect.TypeOf(a))
fmt.Println(reflect.TypeOf(c))
fmt.Println(b)
fmt.Println(c)

OUTPUT: int string A 65

常量与运算符

常量的定义

  • 常量的值在编译时就已经确定
  • 常量的定义格式与变量基本相同
  • 等号右侧必须是常量或者常量表达式
  • 常量表达式中的函数必须是内置函数
  • 常量一般使用全大写字母,像星期 等大家都熟知的可以使用固定写法 如果你想要常量不暴露给外部

常量的初始化规则与枚举

  • 在定义常量组时,如果不提供初始值,则表示将使用上行的表达式 注意:使用相同的表达式不代表具有相同的值
  • 使用上一行的表达式时,注意:第一行不能使用,因为第一行没有上一行 emm
  • 使用上一行表达式时, 常量个数需要对应, 比如:
1
2
3
4
const(
var1, var2 = 1, "f"
var3, var4
)
  • 常量可以单个声明赋值,也可以并行,常量组声明赋值。
  • 全局变量不是常量,不能用于初始化常量。

itoa

iota 是 golang 语言的常量计数器,只能在常量的表达式中使用。

iota 在 const 关键字出现时将被重置为 0(const 内部的第一行之前),const 常量组中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 常量组中的行索引), 注意:iota 在常量组中自动计数,与你使不使用无关。

使用 iota 能简化定义,通过初始化规则与 iota 可以达到枚举的效果 每遇到一个 const 关键字,iota 就会重置为 0

举例如下:

  1. iota 只能在常量的表达式中使用。 fmt.Println(iota) 编译错误: undefined: iota

  2. 每次 const 出现时,都会让 iota 初始化为 0.

    1
    2
    3
    4
    5
    const a = iota // a=0
    const (
    b = iota //b=0
    c //c=1
    )

  3. 使用 iota 模拟计算机的存储单位增加

1
2
3
4
5
6
7
const (
B float64 = 1 << (10 * iota)
KB
MB
GB
TB
)

3、自定义类型

自增长常量经常包含一个自定义枚举类型,允许你依靠编译器完成自增设置。

type Stereotype int

const ( TypicalNoob Stereotype = iota // 0 TypicalHipster // 1 TypicalUnixWizard // 2 TypicalStartupFounder // 3 )

运算符

Go 中的运算符均是从左至右结合

优先级(从高到低)

-^ ! (一元运算符)

  • * / % << >> & &^
  • + - | ^ (二元运算符)
  • == != < <= >= >
  • <- (专门用于 channel)
  • &&
  • ||

两个 ^的区别:

  1. ^ 和一个运算数在一起时,为一元运算符
  2. 一元运算符优先级高于二元运算符

二元位运算操作符:

1
2
3
4
5
6
7
6: 0110
11: 1011
----------
& : 0010 = 2 按位与
| : 1111 = 15 按位或
^ : 1101 = 13 按位异或
&^: 0100 = 4 按位置零

&& :逻辑与, 具有短路作用, 可以通过判断前面条件是否满足, 来决定是否执行后面的操作。 || :逻辑或,也有短路作用

判断语句 if

  • 条件表达式没有括号
  • 支持一个初始化表达式(可以是并行方式)
  • 左大括号必须和条件语句或 else 在同一行
  • 支持单行模式
  • 初始化语句中的变量为 block 级别,同时隐藏外部同名变量(即:同名的局部变量与全局变量在局部会隐藏全局变量)

注意:初始化表达式之间用分号分割。

1
2
3
4
if a, b := 1, 2; a < 2 {
fmt.Println("Hello World")
}

循环语句 for

  • Go 只有 for 一个循环语句关键字,但支持 3 种形式
  • 初始化和步进表达式可以是多个值
  • 条件语句每次循环都会被重新执行、检查,因此,不建议在条件语句中使用函数,尽量提前计算好条件并以变量和常量代替。 比如:字符串比较与 len(str1) 的大小时, 可以先在外面求出 len 的值来,然后放到 i < str_len 那里, 就不用每次循环算了。
  • 左大括号必须和条件语句在同一行使用。

三种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
a := 1
for {
a++
if a > 3 {
break
}
}
fmt.Println(a)
fmt.Println("---------")

b := 1
for b < 3 {
b++
fmt.Println(b)
}
fmt.Println("---------")

for i := 1; i < 5; i++ {
fmt.Println("Here")
if i > 3 {
continue
}
fmt.Println(i)
}
}

选择语句 switch

  • 可以使用任何类型或表达式作为条件语句
  • 不需要写 break,一旦条件符合自动终止
  • 如希望继续执行下一个 case,需使用 fallthrough 语句
  • 支持一个初始化表达式(可以是并行方式),右侧需跟分号
  • 左大括号必须和条件语句在同一行

跳转语句 goto, break, continue

  • 三个语法都可以配合标签使用
  • 标签名区分大小写,若不使用会造成编译错误
  • break 与 continue 配合标签可用于多层循环的跳出, break, continue 如果不跟标签的话,就是只针对内层循环 break LABEL1, 则针对与 LABEL1 同级循环进行 break, 即:break 到与 LABEL1 同级的循环的外面。 continue LABEL1, 则针对与 LABEL1 同级循环进行 continue, 即:跳过本轮循环,去执行和 LABEL1 同级的循环
  • goto 是调整执行位置,与其它 2 个语句配合标签的结果并不相同

数组

  • 定义数组格式:var <varName> [n]type(n > 0)
  • 数组的长度也是类型的一部分,因此具有不同长度的数组为不同类型。
  • 注意:区分指向数组的指针与指针数组
  • 数组在 Go 中为值类型
  • 数组之间可以使用 ==!= 进行比较,但不可以使用 <>
  • 可以使用 new 来创建数组,词方法返回一个指向数组的指针。
  • Go 支持多维数组

当你想在声明数组时,初始化数组的话,可以采用以下方式:

1
2
3
4
5
6
7
8
9
10
// a := [2]int{1, 2} 声明并赋值
// a := [...]int{1,2,3}
func main() {
//var a [2]int
a := [2]int{1, 2}
fmt.Println(a)
for i := 0; i < 2; i++ {
fmt.Println(a[i])
}
}