本文通过Golang helloworld示例开始,介绍Golang的标识符、关键字、操作符、常量、变量、指针等基础概念。
hello world
// package info
package main
import "fmt"
/*
main func
*/
func main() {
// print
fmt.Println("hello world!")
}
$ go build main.go
$ ./main
hello world!
# 显示编译过程
go build -x ./main.go
# 指定编译包名称
go build -o helloworld main.go
# 编译并运行
go run ./main.go
go run -x ./main.go # 指定临时目录编译
# 运行时内存竞争检测,race 存在很大的性能开销,大约是正常程序的十倍
# https://go.dev/blog/race-detector
go run -race main.go
- 编译目录,必须包含
main
函数,使用 go mod,并安装依赖,正常情况下编译包的名称为 go.mod
中 module 的名称
$ go mod init main
go: creating new go.mod: module main
go: to add module requirements and sums:
go mod tidy
$ go mod tidy
$ go build
$ ls
go.mod main main.go
$ ./main
hello world!
基本元素
标识符
标识符:用于给包名、函数、变量、常量、类型、接口等命名,命名规则如下:
- 由字母(Unicode字符)、数字、下划线(_)组成,区分大小写
- 以字母、下划线开头
- 不能是Go语言的关键字、以及预定义的标识符
- 方法名推荐使用驼峰式,尽量使用英文字符
- 可以使用中文定义变量名,不推荐
- 包名推荐全部小写,以
.go
结尾
关键字
关键字(共25个)
- 声明:
- import:导包
- 一般顺序为:
- 点操作
import . "fmt"
Println("hello world")
- 别名
import( f "fmt")
f.Println("hello world")
_
操作
import (_ "github.com/go-sql-driver/mysql")
- package
- 实体声明和定义:
- chan
- 声明常量:const
- 声明函数:func
- 声明接口:interface
interface
的组成分为 类型
和 值
- 扩展,判断 error 是否为 nil,必须类型和值同时都为 nil 的情况下,interface 的 nil 判断才会为 true
- any 是 interface 的别名:
type any = interface{}
参考
- map
- 声明结构体:struct
- 声明类型:type
- 声明变量:var
- 流程控制:break case continue default defer if else for go goto fallthrough range return select switch
预定义的标识符
预定义的标识符:
- 内置常量:true false nil iota(用于枚举类型)
- 内置类型:
- bool:
- 布尔类型,表示真假;长度一个字节(8位)。零值为 false,字面量只能是 true、false
- 逻辑运算:
- 与 &&(aBool && bBool),两个均为 true 时,值为 true
- 或 ||(aBool || bBool),一个值为 true 时,值为 true
- 非 !(!aBool),aBool 的相反值
- 关系运算:
- 等于 == (true == true) 值为 true
- 不等于 != (true != true) 值为 false
- 有符号:int int8(0~2^8-1) int16 int32 int64
- 无符号:uint uint8(-2^7~2^7-1) uint16 uint32 uint64
- 1个Unicode码点:rune(将字符串转化成unicode 码点),使用单引号赋值。示例:
var r rune = '中'
,参考
- rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values.
- 1个字节(8位):byte,使用单引号赋值。示例:
var a byte = 'A'
- 存储指针的整型:uintptr,参考,说明
- int uint uintptr 是32位操作系统为int32(4个字节),64位系统为int64(8个字节)
- 有符号与无符号的区别:
- 有符号最高位0表示整数、1表示负数
- 无符号均为正数
#ifdef _64BIT
typedef uint64 uintptr;
#else
typedef uint32 uintptr;
#endif
- 浮点数,零值为 0,浮点数存储是有精度损耗的(2.5==2.499999…)
- float32
- float64
- 说明:
- 字面量:
- 十进制表示法
- 科学技术法:MEN = M * 10 ^ N,示例:
2.5e-1 == 0.25
- 算术运算符:+,-,*,/,++,–
- 关系运算:>,>=,<,<=, 由于存储精度存在损耗,相等运算可以认为差值<0.00005等方法>
- 赋值运算:=,+=,-=,/=,*=
- 类型转化可能出现溢出
- 复数,如:默认为 complex128
i := 1+2i
- 字符串:string,只能为 ascii 值
- 说明:
- var name string = “xianbin” # 可解释的字符串,包含特殊字符正常展示输出,可以转移输出,如 “\t”
- var desc string =
xianbin,中国人
# 原生字符串,可以包括特殊字符,如 \n,\f,\t,\b…,特殊字符原生输出
- 算术运行符:
- 关系运算:==,!=,>,>=,<,<=
- 赋值运行:
s := "abc"; s += "def"
- 索引:0 ~ n-1(n 为字符串的长度,使用
len(s)
计算长度),仅对ascii有效,否则乱码。name = "xianbin"; fmt.Printf("%T, %c", name[0], name[0]) // "int8 x"
len/cap
是 Go 编译器的魔法
,不是实际的函数调用,参考
- 切片:s[start:end],字符串长度 start ~ end-1,不包括end,仅对ascii有效,否则乱码。
name = "xianbin"; fmt.Printf("%T, %v", name[0:2], name[0:2]) // "string xi"
- error
- 进制:十进制、八进制(0644 = 6x8^2 + 4x8^1 + 4)、十六进制(0x开头,0-9A-F)
- 内置函数:
- append cap copy close complex delete make len real imag panic revocer
- new:创建,返回值是指针类型,一般用于值类型中,不在引用类型中使用
- 空白标识符:_
字面量
字面量:用于变量、常量的初始化
- 基础数据类型,如:0,1.1,true
- 构造自定义复合数据类型,如:
type Name string
- 复合数据类型值,用来构造array,slice,map,struct的值,如{“a”,“b”,“c”}
操作符
- 算术运算符:+,-,*,/(5/2=2),%(5%2=1),++(仅支持放在变量后面:age++),–
- 关系运算符:>,>=,<,<=,==,!=
- 逻辑运算符:
- 位运算(二进制的运算)符:
- 与 & (5&2 = 2)
- 或 | (5|2 = 7)
- 异或 ^ (5^2 = 7)
- 左移 « (2«1 = 4)
- 右移 » (2»1 = 1)
- 位清空(同为1的位清为0) &^ (5&^2 = 5)
- 赋值运算符:=,+=,-=,*=,/=,%=,&=,|=,^=,«=,»=,&^=
- 其他运算符:
- 取地址:&
- 取引用:*
- 方法调用或属性值:.
- 取反:-,a=-1 -> -a=1
- 可变参数:…
- chan 的读和写:<-
分隔符
- 小括号:()
- 中括号:[]
- 大括号:{},用来声明作用域
- 分号:;,每行后可选加;,一行多条语句使用;分隔
- 逗号:,
变量
变量:对一块存储空间的名称定义,可通过该名称对存储空间的内容进行访问、修改等操作,格式如下:
- var <变量名> <变量类型> = <变量值>
- var <变量名> <变量类型>,
var a string
- var <变量名> = <变量值>
- var <变量名1>, <变量名2>,.. <变量类型>
- var <变量名1>, <变量名2> <变量类型> = <变量值1> <变量值2>
- var <变量名1>, <变量名2> = <变量值1> <变量值2>
- 短声明:
<变量名> := <变量值>
,变量类型自动推到。该声明方式只能在函数内部使用,如 b := true
- 说明:
- 函数内定义的变量必须使用,否则编译错误
- 函数外定义的变量可以不使用
- 变量只能声明一次
:=
左侧至少有一个新变量
- 类型后置的语言有:
Go
、Rust
等,Go 省略了 :
,如 let x: i32;
var (
name string = "xianbin"
age int = 18
)
// or
var (
name = "xianbin"
age = 18
)
// switch a, b
var a, b = 1, 9
a, b = b, a
常量
常量:不允许修改的量,变量名一般全大写,必须设置值
const NAME string = "xianbin"
const PI = 3.14
const NAME, AGE = "xianbin", 18
const (
NAME string = "xianbin"
age int = 18
)
const (
NAME = "xianbin"
age = 18
)
// 缺省赋值,同时省略类型和值,表示和前一个常量相同
const (
C1 int = 1
C2
C3
)
fmt.Println(C1, C2, C3) // 1 1 1
// 枚举赋值,同时省略类型和值,表示前一个常量增加1
const (
I1 int = iota
I2
I3
)
const (
I4 int = iota
I5
I6
)
fmt.Println(I1, I2, I3) // 0 1 2
fmt.Println(I4, I5, I6) // 0 1 2
作用域
作用域:用来定义标识符的使用范围,只能被声明一次且变量必须使用,Golang 使用 {}
定义作用域,大括号内的语句称为语句块
- 子语句块可以使用父语句块的标识符
- 父语句块不能使用子语句块的标识符
- 子语句块中可以重复定义父语句块的标识符,并属于两个不同的作用域
- 常见的语句块:
- 包语句块;文件语句块;if、switch、case、for、select语句块
注释
- 单号以 // 开头
- 单号以 /* 开头,以 */ 结尾
- 多行以 /* 开头,以 */ 结尾
类型转换
类型转换,包括 int/int*/uint/uint*/byte/rune 之间的相互转换,从大往小转化可能出现溢出截断,如 var A int = 0XFFFF 使用 uint8(A)会出现截断现象
var intA int = 10
var uintB uint = 2
fmt.Println(intA + int(uintB))
fmt.Println(uint(intA) + uintB)
指针
用于存储变量在内存中的存储位置,指针默认被初始化为 nil
package main
import "fmt"
func main() {
var v1 = 2
var v2 = 3
v2 = v1
fmt.Println(v1, v2) // 2 2
v1 = 4
fmt.Println(v1, v2) // 4 2
// 指针:使用 & 取地址
p1 := &v1
var p2 *int = &v2
fmt.Printf("%T %T\n", p1, p2) // *int *int
// 取值:使用 * 取指针的值
fmt.Println(*p1, *p2) // 4 2
*p1 = 10
*p2 = 15
fmt.Println(v1, v2) // 10 15
}
输入&输出
fmt.Println("...") // 输出一行
fmt.Print("...") // 输出字符,不带换行
源码实现(参考):
// Print prints x to standard output, skipping nil fields.
// Print(fset, x) is the same as Fprint(os.Stdout, fset, x, NotNilFilter).
func Print(fset *token.FileSet, x any) error {
return Fprint(os.Stdout, fset, x, NotNilFilter)
}
格式化输出
fmt.Printf("%s", "...") // 格式化输出,不换行,使用 `\n` 换行
%s
字符串,若出现 %!s(xx=bool),表示使用 %s 输出的类型不匹配
%T
类型
%d
整数
%8d
占8位格式化整数,没有的空格填充
%08d
占8位格式化整数,没有的0填充
%-8d
占8位格式化整数,左对齐(没有-表示右对齐)
%f
浮点数
%6.2f
6位浮点数(包括小数点),保留2位小数
%b
二进制
%o
八进制
%x
十六进制
%t
bool 类型
%c
byte 类型
%p
指针地址
%q
rune (码点)类型,原始值
%U
rune (码点)类型,编码值
%v
调用每种类型自己的格式输出
%#v
打印结构体
%%
输出百分号
%w
格式化 error
输入
从命令行读入数据
package main
import "fmt"
func main() {
var input string
fmt.Print("please input something:")
fmt.Scan(&input)
fmt.Println("you input is", input)
}
注意:
- 若输入的类型与期望的类型不匹配,将会赋予对应类型的零值
流程控制
条件 if-else
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
r := rand.Intn(10)
if r >= 8 {
fmt.Printf("%v >= 8", r)
} else if r >= 5 {
fmt.Printf("5 =< %v < 8", r)
} else {
fmt.Printf("%v < 5", r)
}
}
选择 switch-case
// 值判断
switch var1 {
case val1:
case val2:
default:
}
// 条件判断
switch {
case conditionA:
case conditionB:
default:
}
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
r := rand.Intn(10)
// 值判断
switch r {
case 10:
fmt.Printf("%v == 8\n", r)
case 9, 8:
fmt.Printf("%v == 8 or %v == 9\n", r, r)
default:
fmt.Printf("%v < 8\n", r)
}
// 条件判断
switch {
case r >= 8:
fmt.Printf("%v == 8", r)
case r >= 5:
fmt.Printf("5 =< %v < 8", r)
default:
fmt.Printf("%v < 5", r)
}
}
package main
import "fmt"
func main() {
isMatch := func(i int) bool {
switch i {
case 1:
case 2:
return true
}
return false
}
fmt.Println(isMatch(1))
fmt.Println(isMatch(2))
}
// false, true
package main
type AuthType int
const (
A AuthType = iota
B
C
)
func (f AuthType) String() string {
return [...]string{"A", "B", "C"}[f]
}
func main() {
var f AuthType = A
fmt.Println(f)
switch f {
case A:
fmt.Println("A")
case B:
fmt.Println("B")
default:
fmt.Println("...")
}
}
select
select
是 golang 实现的多路选择操作符,结构类似于 switch
,一般用来监听和 channel
有关的 IO 操作,当 IO 操作发生时,触发相应的 case 动作
- 多个
case
之间并非顺序的,同时满足时,随机选择一个执行
default
为可选的
select {
case <-ch1: // 检测有没有数据可读
// 一旦成功读取到数据,则进行该case处理语句
case value := <-ch1: // 检测有没有数据可读
// 一旦成功读取到数据,则进行该case处理语句
case ch2 <- 1: // 检测有没有数据可写
// 一旦成功向ch2写入数据,则进行该case处理语句
default:
// 若以上都没有符合条件,则进入default处理流程
}
// Select case descriptor.
// Known to compiler.
// Changes here must also be made in src/cmd/compile/internal/walk/select.go's scasetype.
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}
以下代码造成死锁
package main
func main() {
select {
}
}
循环 for
for 初始化子语句; 条件子语句;后置子语句 {
...
}
for condition {
}
for {}
for range
package main
import "fmt"
func main() {
sum := 0
for i := 1; i <= 10; i++ {
sum += i
}
fmt.Println(sum)
sum = 0
i := 1
for i <= 10 {
sum += i
i++
}
fmt.Println(sum)
sum = 0
i = 0
for {
sum += i
if i == 10 {
break
}
i++
}
fmt.Println(sum)
}
for range
适用于字符串、数组、切片、映射、管道等 ascii 编码
package main
import "fmt"
func main() {
message := "我是中文"
for index, ch := range message {
fmt.Printf("%d %T %q\n", index, ch, ch)
}
}
输出:
0 int32 '我'
3 int32 '是'
6 int32 '中'
9 int32 '文'
说明:
rune
类型也是 int32
- 字符使用单引号,字符串使用双引号
break & continue
- break 跳出循环
- continue 跳过本次循环
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
if i == 4 {
continue // 跳过 4
} else if i == 6 {
break // 跳出循环
}
fmt.Println(i)
}
}
package main
import "fmt"
func main() {
END:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j == 9 {
break END
}
fmt.Printf("%v * %v = %v\n", i, j, i*j)
}
}
fmt.Println("end") // 总会执行
}
continue 同样有次用法,label 必须定义在循环的上面
goto
作用跳转到指定位置,可用于多层嵌套的for中
-
标签定义:大写英文字符 + “:”
-
goto 标签:实现跳转到指定位置
-
goto 示例1
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
r := rand.Intn(10)
if r > 5 {
goto GT5
} else {
goto END
}
GT5:
fmt.Println("gt 5")
END:
}
package main
import "fmt"
func main() {
i := 1
sum := 0
START:
if i > 10 {
goto END
}
sum += i
i++
goto START
END:
fmt.Println(sum)
}
error
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
var (
ErrInvalidParam = errors.New("invalid param")
)
if err != nil && err == ErrInvalidParam {
//...
}
// 判断带参数的 error
if errors.Is(err, ErrInvalidParam) {
//..
}
// `errors.Unwrap` 用来将错误中将原错误解析出来,`fmt.Errorf` 包装的 `error` 使用 `%w` 格式化
err := fmt.Errorf("origin error: %w", errxxx)
err = errors.Unwrap(err)
fmt.Printf("origin error: %+v\n", err)
interface
interface 有两类数据结构:
runtime.eface
结构体:表示不包含任何方法的空接口,也称为 empty interface
runtime.iface
结构体:表示包含方法的接口
nil
- 参考
- 说明
- struct 的零值与其属性有关,不是
nil
- 不同类型
nil
的内存地址是一样的
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
package main
import (
"fmt"
)
func main() {
var m map[int]string
var ptr *int
var sl []int
fmt.Printf("%p\n", m) // 0x0
fmt.Printf("%p\n", ptr ) // 0x0
fmt.Printf("%p\n", sl ) // 0x0
}
内存分配
栈(Stack)
由编译器进行管理,自动申请、分配、释放
- 一般存放的对象不会太大,如函数参数、局部变量等放在栈上
堆(Heap)
一般代码进行管理,显示申请、分配、释放
- 一般存放较大的对象,分配相对慢,涉及到的指令动作也相对多,可采用
go build -gcflags '-m -l' x.go
go tool compile -S x.go
查看反编译命令
gc 垃圾回收
golang 垃圾回收有3种方式(参考):
gcTriggerHeap
当所分配的堆大小达到阈值时触发
gcTriggerTime
当距离上一个 GC 周期的时间超过一定时间时,将会触发
- 时间周期以
runtime.forcegcperiod
变量为准,默认 2 分钟
gcTriggerCycle
未启动时,手动触发
// A gcTrigger is a predicate for starting a GC cycle. Specifically,
// it is an exit condition for the _GCoff phase.
type gcTrigger struct {
kind gcTriggerKind
now int64 // gcTriggerTime: current time
n uint32 // gcTriggerCycle: cycle number to start
}
type gcTriggerKind int
const (
// gcTriggerHeap indicates that a cycle should be started when
// the heap size reaches the trigger heap size computed by the
// controller.
gcTriggerHeap gcTriggerKind = iota
// gcTriggerTime indicates that a cycle should be started when
// it's been more than forcegcperiod nanoseconds since the
// previous GC cycle.
gcTriggerTime
// gcTriggerCycle indicates that a cycle should be started if
// we have not yet started cycle number gcTrigger.n (relative
// to work.cycles).
gcTriggerCycle
)
入口函数
- 编译命令:
GOFLAGS="-ldflags=-compressdwarf=false" go build
- Mac 的入口
src/runtime/rt0_darwin_amd64.s
- Linux 的入口
src/runtime/rt0_linux_amd64.s
m0
主线程是 Go Runtime
创建的第一个系统线程
g0
每一个 m
只有且仅一个 g0
,且每个 m
都只会绑定一个 g0