配置 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{}