Golang 基础知识

本文整合了 Golang 的基础知识体系,包括基础语法规范、包管理(packageimport) 以及特有的指针概念,适合 Go 语言初学者参考。

1. 简介

GO 语言特点

  1. 运行效率高,开发高效,部署简单 (可直接编译成机器码,不依赖其他库,glibc的版本有一定要求,部署就是扔一个文件上去就完成了)
  2. 语言层面支持并发,易于利用多核实现并发 (最大特点)
  3. 内置 runtime(作用:性能监控,GC 等),支持垃圾回收
  4. 丰富标准库,强大网络库
  5. 内置强大的工具( gofmt ),跨平台编译,内嵌 C 支持

GO 语言的应用

  1. 服务器编程,以前你如果使用 C 或者 C++ 做的那些事情,用 Go 来做很合适,例如处理日志、数据打包、虚拟机处理、文件系统等。
  2. 分布式系统,数据库代理器等
  3. 网络编程,这一块目前应用最广,包括Web应用、API应用、下载应用、
  4. 内存数据库,前一段时间 google 开发的 groupcache,couchbase 的部分组建
  5. 云平台,目前国外很多云平台在采用 Go 开发,CloudFoundy 的部分组建,前 VMare 的技术总监自己出来搞的 apcera 云平台。

GO 语言命令行工具

  1. go build 用于编译源码文件,代码包,依赖包;
  2. go run 可以编译并运行Go源码文件
  3. go get 主要用来动态获取远程代码包

编译 windows x86_64 GOOS=windows GOARCH=amd64 go build hello.go

GO 安装

使用 linux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cd /usr/src
wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz
tar -xvf go*.linux-amd64.tar.gz -C /usr/local/

# 定界符必须加引号写成 << 'EOF'。不加的话里面的变量会在**写文件的那一刻**就被展开,
# 而 GOROOT/GOPATH/GOBIN 此时都还没定义,实测生成出来的是:
# export GOBIN=/bin
# export PATH=/usr/bin:/bin:
# /usr/local/go/bin 从来没进过 PATH(末尾还多出一个空条目,等于把当前目录加进了 PATH),
# 紧接着的 go version 必然 command not found。
#
# 另外 GOBIN 不是"go 命令所在的目录",而是 go install 的**产物安装目录**(默认 $GOPATH/bin)。
# 指到 $GOROOT/bin 会把第三方工具装进 Go 发行版目录里,污染 GOROOT。
cat > /etc/profile.d/go_env.sh << 'EOF'
export GOROOT="/usr/local/go"
export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOROOT/bin:$GOBIN
EOF

source /etc/profile
go version
go env

一些配置

1
2
3
4
go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct
go env -w GO111MODULE=on

go env -w GOPRIVATE="*.example.com"

2. 基础语法

一般结构

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
// 当前程序的包名
package main

// 导入其他包
import . "fmt"

// 常量定义
const PI = 3.14

// 全局变量的声明和赋值
var name = "gopher"

// 一般类型声明
type newType int

// 结构的声明
type gopher struct{}

// 接口的声明
type golang interface{}

// 由main函数作为程序入口点启动
func main() {
Println("Hello World!")
}

变量

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main
import "fmt"

var x1, y1 int
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a1 int
b1 bool
)

var c1, d1 int = 1, 2
var e1, f1 = 123, "hello"

var h1 int // 全局变量 允许声明但不使用
// e1 := "weilai" // 只能被用在函数体内,而不可以用于全局变量的声明与赋值


func main() {
/*
多行注释
*/
// 一行注释
var a string = "weilai" // 声明一个变量并初始化
var b, c int = 1, 2 // 一次声明多个变量:
var d int // 声明变量,不初始化,一般情况下,值类型默认为 0, 布尔类型默认值为 false ,string 默认值为空字符串 `""`(Go 里单引号是 rune 字面量,`''` 根本不是合法写法)
var z bool // bool 0值 false
fmt.Println(z)
fmt.Println(a) // 打印 a 换行
fmt.Print(b, c) // 不会换行
fmt.Println(d)
fmt.Println("hello") // 会换行
fmt.Println("world" )
fmt.Print("hello")
fmt.Print("world")
// 输出 "hello" 紧接 "world",中间没有空格 —— 见下面说明

// 上面 fmt.Print(b, c) 输出的是 "1 2",中间有空格;
// 而 fmt.Print("hello") 接 fmt.Print("world") 输出的是 "helloworld",没有空格。
// 这不是笔误,规则见本节末尾。

// var e int 这行存在 下行就会报错就会报错 no new variables on left side of :=
e := "weilai" // 等同于 var e string = "weilai" // 使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明
f, g := 20, 30
f, g = g, f // 如果你想要交换两个变量的值,,两个变量的类型必须是相同。

// h := 40 // 声明但未使用 局部变量 会报错
fmt.Println("e=", e , ",f=" , f , ",g=" , g)
fmt.Printf("e=%s,f=%d,g=%d" , e , f , g) // Printf 是格式化输出
var vname1, vname2, vname3 = "v1", "v2", "v3"
fmt.Printf(vname1, vname2, vname3)
println(x1, y1, a1, b1, c1, d1, e1, f1)

_,numb,strs := numbers() //只获取函数返回值的后两个
fmt.Println(numb,strs)
}
//一个可以返回多个值的函数
func numbers()(int,int,string){
a , b , c := 1 , 2 , "str"
return a,b,c
}

/*

false
weilai
1 20
hello
world
helloworlde= weilai ,f= 30 ,g= 20
e=weilai,f=30,g=20v1%!(EXTRA string=v2, string=v3)0 0 0 false 1 2 123 hello
2 str

*/

上面那两行输出看着自相矛盾,其实是三条不同的规则:

fmt.Print:仅当相邻两个操作数都不是字符串时才补空格。

1
2
3
4
fmt.Print(1, 2)          // "1 2"   相邻两者都不是 string → 补空格
fmt.Print("a", "b") // "ab" 都是 string → 不补
fmt.Print("a", 1, "b") // "a1b" 每一对里都有 string → 都不补
fmt.Print(1, "a", 2) // "1a2" 同上

所以 fmt.Print(b, c) 打印 1 2(都是 int),而两次 fmt.Print("hello") / ("world") 是两次独立调用,之间当然也没有空格 —— 得到 helloworld。这条规则很反直觉,是 Print 系列最容易踩的地方。

fmt.Println:无条件在每个操作数之间补空格,末尾加换行。 不管是什么类型。所以想要稳定的分隔就用 Println,或者干脆 Printf 自己写格式。

内置的 println(不带 fmt.)是另一回事。 它是编译器内置函数,不需要 import:

  • 输出到 stderr 而不是 stdout —— 所以 go run x.go > out.txt 时它的内容不会进文件;
  • 语言规范明确说它的行为不保证,编译器可以自由实现,也可能在未来被移除;
  • 不支持任何格式化动词。

它只适合临时插桩调试,正式代码里不该出现。

别把变量当格式串

1
fmt.Printf(vname1, vname2, vname3)     // 不要这样写

Printf 的第一个参数是格式串。传一个普通变量进去,里面没有任何 % 动词,后面两个参数就无处安放,fmt 会在输出末尾追加一个错误标记:

1
v1%!(EXTRA string=v2, string=v3)

go vet 会把这种写法报成 printf: non-constant format string in call to fmt.Printf(Go 1.24 起对此更严格,因为格式串来自变量时还可能引入格式化注入)。要原样打印多个值就用 fmt.Println(vname1, vname2, vname3)

顺带认一下 fmt 的错误标记体系,看到它们就知道是哪类问题:

标记 含义
%!(EXTRA type=value, ...) 参数比动词多
%!v(MISSING) 动词比参数多
%!d(string=abc) 动词和参数类型不匹配(这里想打整数却给了字符串)
%!(BADINDEX) 显式参数索引 %[3]d 越界
%!(NOVERB) % 后面没跟动词

3. 规范

变量

声明,初始化,赋值

  1. 同一行声明多个变量并赋值: var a,b,c int =1,2,3 或者 d,e := 4,5

  2. 全局变量声明必须使用var关键字,局部变量可以用短变量声明省掉它:a, b, c := 1, 2, 3
    注意省掉 var 之后就不能再写类型了——a, b, c int = 1, 2, 3 在函数体里是语法错误,
    要显式标类型就得把 var 写回来:var a, b, c int = 1, 2, 3

  3. 变量 _ 表示丢弃

变量类型转换

  1. 类型转换必须是显示的,不存在隐式转换
  2. 只发生在兼容类型之间
  3. <变量名称> [:]= <目标类型>(<需要转换的变量>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
"reflect"
)

func main(){

var a float32 = 3.1
c := int(a)

fmt.Println(c)
fmt.Println(reflect.TypeOf(c))

}

变量可见性规则

  1. 大写字母开头的变量可导出,其他包可以读取,是共有变量
  2. 小写字母开头的变量不可导出,是私有变量

常量

常量定义形式

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

  1. 显示 const identifier [type] = value
  2. 隐式 const identifier = value (通常叫做 无类型常量)
  3. 常量可以使用 内置表达式定义 eg: len(),unsafe.Sizeof()

特殊常量 iota

  1. iota 在 const 关键字出现时被重置为 0
  2. const 中每新增一行常量声明 iota 就计数一次
  3. iota 只能在常量中使用

iota 常见使用法:

  1. 跳值使用法
  2. 插队使用法
  3. 表达式隐式使用法
  4. 单行使用法

4. package与import

package

  1. package 是最基本的分发单位 和 工程管理中依赖关系的体现
  2. 每个 GO 语言源代码文件开头 都拥有一个 package 声明,表示源代码所属的代码包
  3. 要生成 GO 语言可执行程序,必须要有 mainpackage 包,且必须在该包下 有 main() 函数
  4. 同一个路径下(文件夹) 只能存在 一个 package, 一个 package 可以拆分为多个源文件组成

import 导入包的几种方式:点,别名与下划线

在写 Go 代码的时候经常用到 import 这个命令用来导入包文件,看到的方式参考如下:

1
2
3
import(
"fmt"
)

然后在代码里面可以通过如下的方式调用
fmt.Println("hello world")

上面这个fmt是Go语言的标准库,他其实是去 GOROOT 下去加载该模块,当然 Goimport 还支持如下两种方式来加载自己写的模块:

相对路径 import "./model" // 当前文件同一目录的 model 目录,但是不建议这种方式 import

绝对路径 import "shorturl/model" // 加载GOPATH/src/shorturl/model模块

三种导入包的使用方法。

  1. 点操作 import( . "fmt" )
    可以省略前缀的包名,fmt.Println("hello world") 可以省略的写成 Println("hello world")

  2. 别名操作import( f "fmt" ) 调用包函数时前缀变成了重命名的前缀,即 f.Println("hello world")

  3. _ 操作import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )

_ 操作其实只是引入该包。即使用 _ 操作引用包是无法通过包名来调用包中的导出函数,而是只是为了简单的调用其 init 函数()


5. 指针

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

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

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var a int = 10

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

指针声明 : var var_name *var-type eg: var fp *float32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr ) /* 0 */
// %x 用在指针上是合法的(按整数格式化),nil 得到 0;
// 但工程上应该统一用 %p,它带 0x 前缀、nil 时打印 0x0;%v 对 nil 指针打印 <nil>
}