配置 Go 工作区
继续之前,请务必仔细阅读此部分。
Go 在组织项目文件方面与其他编程语言不同。 首先,Go 是在工作区的概念下工作的。 工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一个工作区。 不过,从版本 1.11 开始,Go 已开始更改此方法。 你尚且不必担心,因为我们将在下一个模块中介绍工作区。 现在,Go 工作区位于 $HOME/go,但如果需要,可以为所有项目设置其他位置。
若要将工作区设置为其他位置,可以使用 $GOPATH 环境变量。 在处理更复杂的项目时,此环境变量有助于避免将来出现问题。
Go 工作区文件夹
每个 Go 工作区都包含三个基本文件夹:
- bin:包含应用程序中的可执行文件。
- src:包括位于工作站中的所有应用程序源代码。
- pkg:包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。
例如,工作站文件夹结构树可能与下面的示例类似:
bin/ hello coolapp pkg/ github.com/gorilla/ mux.a src/ github.com/golang/example/ .git/ hello/ hello.go
Go实战经验
在命令行中输入’code . ‘会唤起VS code编辑当前目录
源码规范
- 可执行文件都要包含在package main中
- import的包必须都要使用,否则报错不进行编译;vs code中保存文件就会自动调整文件格式,并且删除未使用的import
- 整个package main中只能有一个func main()
变量的声明和初始化
Go是强类型语言,声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。
变量声明的方式有很多,格式和其他语言不太一样
- 最普通的方式:var 变量名称 变量类型
- Go也可以像Python那样自动推断变量的类型,有些时候可以不用加类型名称
- 最常用的方式(只适用于在函数内,声明并初始化一个新的变量):使用冒号等号 age := 32
- 注意,在函数体外还是只能用var的方式声明和初始化变量
// 变量声明
// 变量声明了必须要使用,否则编译不通过
var first string
var second, third string
var age int = 1
var (
fisrtly int = 1
secondly string = 2
thirdly = "123"
)
var firstName, secondName, agenumber = "123", "456", 32
// 最常见的声明方式 冒号等于号 := 用于声明并初始化变量,不能用于常量的声明
firstName_, secondName_, age_ := "123", "456", 32
// 常量声明f
const HTTPstatusOK = 200
const (
StatusOK = 0
StatusConnectionReset = 1
StatusOtherError = 2
)
数据类型
- 基本类型:数字、字符串、布尔值
- 聚合类型:数组、结构体
- 引用类型:指针、切片、映射、函数、通道
- 接口类型:接口
基本类型
在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。
- int 类型的 0(及其所有子类型,如 int64)
- float32 和 float64 类型的 +0.000000e+000
- bool 类型的 false
- string 类型的空值
整数
int类型有int8, int16, int32, int64, uint8等,不同类型的数字之间进行运算需要进行显式转换
uint,无符号整数一般只用于位运算符和特定的算术运算符,如实现位集,解析二进制格式文件,或散列和加密。
rune 只是 int32 数据类型的别名,常常指代一个Unicode码点(code point);
byte类型是uint8类型的同义词,强调一个值是原始数据,而非量值。
算术运算符
- 取模运算符%,仅能用于整数,取模余数的正负号总是和被除数一致。
- 除法运算/,其行为取决于操作数是否都为整型,整数相除,商会舍弃小数部分。
全部的基本类型的值(布尔值、数值、字符串)都可以进行比较。
位运算符
- 运算符^作为二元运算符时,表示异或(XOR);作为一元运算符时,表示按位取反。
- 运算符&^表示按位清除: 如表达式z=x&^y,可以理解为y先取反,再与x做AND。
格式化输出
o := 0666
fmt.Printf("%d %[1]o %#[1]x %#[1]X",o)
- 谓词%d、%o、 %x和%X,指定进位制基数和输出格式;
- %后面的副词[1]高速Printf重复使用第一个操作数;
- 副词#高速Printf输出相应的前缀0、0x或0X。
- 用%c输出文字符号(rune literal),如果希望有单引号输出,用%q
浮点数
Go具有两种大小的浮点数:float32和 float64。
在十进制下,float32的有效位数大约是6位,float64有效位数大约是15位,绝大多数情况下,应该优先选择float64。
复数
Go具备两种大小的复数complex64和complaex128,两者分别由float32和float64构成。
内置的complex函数根据给定的实部和虚部创建复数complex(1,2)
内置的real和imag函数分别提取复数的实部和虚部,real(x*y),imag(x*y)
可以使用==和!=判断复数是否等值,若两个复数的实部和虚部都相等,则他们相等。
math/cmplx包提供了复数运算所需的库函数,如复数的平方根函数和幂函数。
布尔值
在 Go 中,不能将布尔类型隐式转换为 0 或 1,反之也不行。
字符串
字符串是不可变的字节序列。
字符串操作的4个重要标准包
strings
strings包提供了很多用于搜索、替换、比较、修整、切分和连接字符串的函数。
bytes
主要用于操作字节slice([]byte类型,其某些属性和字符串相同)
strconv
主要用于转换布尔值、整数、浮点数为 与之对应的 字符串形式;
或者把 字符串 转换为 布尔值、整数、浮点数;
另外还有为字符串添加、去除引号的函数。
unicode
主要用于判别文字符号值特性的函数,如IsDigit、IsLetter、IsUpper和IsLower。
字符串与数字的相互转换
int64(integer32)这样转换
使用包strconv,实现字符串和数字之间的转换
func Atoi(s string) (int, error)
Atoi is equivalent to ParseInt(s, 10, 0), converted to type int.
func Itoa(i int) string
Itoa is equivalent to FormatInt(int64(i), 10).
整数转换成字符串
fmt.Sprintf("%d",x)strconv.Itoa(x)
解释表示整数的字符串
x, err := strconv.Atoi("123")y, err := strconv.ParseInt("123",10,64)
常量
常量是一种表达式,可以保证在编译阶段就可以计算出其表达式的值。
常量声明可以同时指定类型和值,如果没有显示指定类型,则类型根据右边的表达式推断。
常量生成器iota
在常量声明中,iota从0开始取值,逐项加一。
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
上面声明中,Sunday的值为0,Monday为1…
无类型常量
许多常量并不从属于某一具体类型,这些值比基本类型的数字精度更高,且算术精度高于原生的及其精度,可以认为他们的精度至少达到了256位。
借助推迟确定丛书类型,无类型常量不仅能够能暂时维持更高的精度,与类型已确定的常量相比,他们还能写进更多表达式而无需转换类型。
例如无类型的浮点型常量math.Pi可用于任何需要浮点值或者复数的地方:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
复合数据类型
数组和结构体都是聚合类型,他们的长度都是固定的。
slice和map都是动态数据结构,他们的长度在元素添加到结构中时可以动态增长。
数组
如果一个数组的元素类型是可比较的,那么这个数组是可比较的,可以用==比较两个数组。
Go把数组和其他的类型都看成值传递,而在其他的语言中,数组是隐式地使用引用传递。
当然,也可以传递一个数组的指针给函数。
slice
slice表示一个拥有想同类型元素的可变长度的序列。slice通常写成[ ]T,其中元素的类型都是T。
数组和slice紧密关联,slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素,而这个数组被称为slice的底层数组。
属性
slice有三个属性:指针、长度、容量。
指针指向数组的第一个可以从slice中访问的元素。
长度是指slice中的元素个数,他不能超过slice的容量。
容量的大小通常是从slice的其实元素到底层数组的最后一个元素的个数。
Go的内置函数len和cap可以返回slice的容量和长度。
slice比较
和数组不同,slice无法作比较,不能用==来测试两个slice是否拥有相同的元素。
标准库中提供了高度优化的函数butes.Equal来比较两个字节slice([ ]byte),但对于其他类型的slice我们必须自己写函数来比较。
slice的零值是nil,检查一个slice是否是空,正确的做法是len(s)==0,而不是s == nil,因为在s != nil的情况下,slice也有可能是空。
make
内置函数make可以创建一个具有指定元素类型、长度和容量的slice。
make([]T,len)
make([]T,len,cap) // 和make([]cap)[:len]功能相同
第一行代码其实make创建了一个无名数组并返回了他的一个slice,这个数组只能通过slice进行访问,其中的cap可以省略,不过slice的长度和容量相等。
第二行代码,slice值引用了数组的前len个元素,未来的slice元素留出空间。
append函数
内置函数append将元素追加到slice的后面。
map
结构体
函数
Go 中的所有可执行程序都具有此函数,因为它是程序的起点。 程序中只能有一个 main() 函数。 如果创建的是 Go 包,则无需编写 main() 函数。
命令行参数
- os.Args[0]是命令本身的名字,从1开始读取命令行参数
- strconv.Atoi()函数的使用,它的返回值是两个,_在Go中表示不会用到的变量
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
// 这里的os.Args[0]是命令本身的名字,所以我们从os.Args[1]开始处理
sum := sum(os.Args[1], os.Args[2])
fmt.Println("Sum:", sum)
}
func sum(a string, b string) int {
// Atoi返回的是两个值,一个是转换后的值,一个是错误信息
numa, _ := strconv.Atoi(a)
numb, _ := strconv.Atoi(b)
return numa + numb
}
函数返回值的强制转换
func sum(number1 string, number2 string) (result int) {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
result = int1 + int2
return
}
返回多个值
在第一行就相当于sum和mul已经完成了声明,所以在后面不能用冒号等号运算符。
func caculate(num1 string, num2 string) (sum int, mul int) {
numa, _ := strconv.Atoi(num1)
numb, _ := strconv.Atoi(num2)
sum = numa + numb
mul = numa * numb
return
}
指针传值
在实参上用 &变量 的方式传递地址,形参用 *类型 的方式接收地址,在函数内部用 *变量 的方式读取地址中的值并进行修改。
package main
import (
"fmt"
"strconv"
)
func main() {
name := "Otis"
fmt.Println("Hello", name)
changeName(&name, "Maeve")
fmt.Println("Hello", name)
}
func changeName(name *string, newName string) {
*name = newName
return
}
包
当使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(具有 .a 扩展名的文件)。
创建包
不同于其他编程语言,Go 不会提供 public 或 private 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:
- 如需将某些内容设为专用内容,请以小写字母开始。
- 如需将某些内容设为公共内容,请以大写字母开始。
package calculator
var logMessage = "[LOG]"
// Version of the calculator
var Version = "1.0"
func internalSum(number int) int {
return number - 1
}
// Sum two integer numbers
func Sum(number1, number2 int) int {
return number1 + number2
}
让我们看一下该代码中的一些事项:
- 只能从包内调用
logMessage变量。 - 可以从任何位置访问
Version变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。) - 只能从包内调用
internalSum函数。 - 可以从任何位置访问
Sum函数。 建议你添加注释来描述此函数的用途。
若要确认一切正常,可在 calculator 目录中运行 go build 命令。 如果执行此操作,请注意系统不会生成可执行的二进制文件。
创建模块
在编写完成包之后,在终端中执行命令go mod init 模块名字,就会将当前的go文件打包成一个模块,想要调用这个模块中的包的内容,就要先import模块中的包名。
控制流
if/else表达式语句
go的if条件没有小括号,但是执行体部分有大括号,又不像Python,又不像C++的。
else子句可选,go不支持三元运算符
像这样:
if i > 0 {
return true
}
// if, else if, else的使用方法
var a int = 10
if a < 20 {
fmt.Println("a is less than 20")
} else if a < 30 {
fmt.Println("a is less than 30")
} else {
fmt.Println("a is greater than 30")
}
// 这里的b是局部变量,只在if语句块中有效,和if条件用的分号隔开
if b := 20; b < 30 {
fmt.Println("b is less than 30")
}
在 Go 中,在 if 块内声明变量是惯用的方式。 这是一种使用在 Go 中常见的约定进行高效编程的方式。
Switch语句
switch语句的基本格式:
switch i{
case 0:
fmt.Println("Zero")
case 1,3,5,7: // case 中可以包含多个值,可以避免代码重复的问题
fmt.Println("One")
default:
fmt.Println("NO MATCH")
}
for语句
// for循环的大括号是必须的
for i := 0; i < 10; i++ {
fmt.Println("for loop")
}
// go没有while循环,可以用for代替
for a < 20 {
fmt.Println("while loop")
a++
}
// 无限循环
var num int64
for {
fmt.Println("infinite loop")
num = time.Now().Unix()
if num%5 == 1 {
fmt.Println("break")
break
}
}
fmt.Println(num)
性能优化
slice
预分配内存
尽可能在使用make()创建切片的时候提供容量信息。
// good
data := make([]int, 0, size)
// bad
data := make([]int, 0)
这和slice的底层实现有关,提前预分配内存可以避免向slice中append的过程中不断扩容,而降低效率。
大内存释放
在已有切片的基础上创建切片,会对原先的切片造成引用,从而不会垃圾回收浪费内存。
可以使用copy替代re-slice。
// bad
func GetLastBySlice(origin []int) []int{
return origin[len(origin)-2:]
}
// good
func GetLstByCopy (origin []int) []int{
result := make([]int,2)
copy(result, origin[len(origin)-2:])
return result
}
string
字符串拼接处理
可以有三种方式处理拼接字符串,直接使用+进行拼接操作,使用StrBuilder()方法,使用ByteBuffer()方法。
func Plus(n int, str string) string {
s := ""
for i:=0; i<n; i++{
s += str
}
return s
}
// 性能最好
func StrBuilder(n int, str string) string{
var builder string.Builder
for i:=0; i<n; i++{
builder.WriteString(str)
}
return builder.String()
}
// 性能和StrBuilder差不多,稍差
func ByteBuffer(n int, str string) string{
buf := new(bytes.Buffer)
for i:=0; i<n; i++{
buf.WriteString(str)
}
return buf.String()
}
字符串string在go中是不可变类型,每次使用+进行拼接都要重新开辟空间,并重新分配内存,这就导致了他的效率很低。
StrBuilder和ByteBuffer底层都是使用[]byte数组,有内存扩容策略,不用每次都重新分配内存。
StrBuilder在转化为字符串的时候直接将底层的[]byte转换成伟字符串进行返回;
而ByteBuffer在转换为字符串的时候重新申请了一块空间,导致他比StrBuilder慢一点。
性能进一步提升
在StrBuilder和ByteBuffer方法中,提前使用builder.Grow(n*len(str))或buf.Grow(n*len(str))方法进行内存预分配。
func StrBuilder(n int, str string) string{
var builder string.Builder
// new
builder.Grow(n*len(str))
for i:=0; i<n; i++{
builder.WriteString(str)
}
return builder.String()
}
struct
空结构体
空结构体不占用任何的内存空间。
可在各种场景当做占位符使用:
- 节省内存
对于实现set,可以考虑用map进行代替;
对于这个场景,只需要用到map的键,而不需要使用值
func EmptyStructMap(n int){
m := make(map[int]struct{})
for i:=0; i<n; i++{
m[i] = struct{}{}
}
}
多线程操作
atimic包 VS 加锁
使用atomic包
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作通过硬件实现,效率比锁高
- sync.Mutex应该用于保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interface{}