Go基本知识

63 阅读56分钟

println print line 输出一行

Println和fmt.PrintLn

版本: go version go1.21.4 windows/amd64

go 常见命令

go build [文件] , 编译并生成可执行文件

go run [文件] 运行go程序,编译成机器码在运行程序,但不会生成可执行文件

go mod init [项目名/模块名] : 初始化项目: 会创建go.mod,是一个包管理工具

go get [Repository][版本号@1.8.0] : 类似npm i [包名],下载并安装包与其依赖

例子:go get github.com/go-redis/redis

go install 编译与安装包

go clean 删除源码包和关联源码包里编译生成的文件

go doc 方法名 查看某方法文档

go env 查看Go项目环境变量

go fmt 格式化代码(是指代码格式化,不是删除)

go version 查看Go当前版本

go tool 查看Go工具

go list -m all 查看项目依赖的包

go list -m -versions 包地址 作用: 查看这个包可用的版本

go包网址: pkg.go.dev/

环境变量

控制台输入go env

环境变量名参数作用
GO111MODULEoff、on、auto设置依赖管理方式
GOPROXYgoproxy.cn.direct goproxy.io.direct...设置镜像源

设置环境变量值

go env -w 环境变量名=环境变量值

import

作用:导入包

使用方式: import 项目(模块)名/包路径

例子: import test/calculator

tip: 当包所在的目录名与包声明的包名不一致时,导入还是包路径,但使用的是package声明的包名

// logic.go文件,所在包径:calculator/logic.go
package test
func Plus(a int, b int) int {
	return a + b
}
package main

import (
	"fmt"
	 "test/calculator"
)

func main() {
	result1 := test.Plus(1, 2)
	fmt.Println("Result1:", result1)
}

别名

import test "test/calculator"

test就是别名

匿名别名

当别名是“_”的时候,会自动执行包里的init函数,用于初始化

点别名 .

当别名是“.”的时候,意思是省略包名,可以直接调用函数。不推荐使用。

例如:Plus(1, 2)可以直接执行,不需要test.Plus(1, 2)。

声明变量与赋值

使用var进行声明

// 相同类型的变量可以写在同一行
var firstName, lastName string
var age int
// 等价于
var (
    firstName, lastName string
    age int
)

初始化赋值

类型可以不写,Go会自动进行推断,叫类型推断

var (
    firstName = "John"
    lastName  = "Doe"
    age       = 32
)
// 等价于
var (
    firstName, lastName, age = "John", "Doe", 32
)

短变量声明

使用 := ****省略var关键字以及显式类型定义,类型go会自动推断类型。

特性:

  • 只能在函数内使用,并且必须是新变量。
  • 外部不能使用,因为外部不能进行赋值,只能声明。
// 在函数内使用
func main() {
    firstName, lastName := "John", "Doe"
	age := 32
    fmt.Println(firstName, lastName, age)
}

匿名变量

使用" _ "作为变量名叫做匿名变量,用于表示不使用的变量。

存在这个的原因是因为Go声明变量就必须要使用,否则会报错,而这个反过来,不可以被使用。

作用:用于某些场景下不得不声明变量但不需要使用。

例子:

package main

import (
	"fmt"
)
//如果我不需要第一个参数,但是又必须被声明
func main() {
	// var x, y, z = getXYZ()
    var _, y, z = getXYZ()
	fmt.Println("Result1:", y, z)
}

func getXYZ() (int, string, bool) {
	return 1, "abc", true
}

tips:

  1. 变量声明后,变量必须得被使用,否则会报错
  2. 在函数外必须使用var声明,不能使用:=
  3. :=只能用于变量,并且只能在函数内使用,且必须是新变量

声明常量

使用const进行声明

//单个常量
const COUNT = 100

// 批量声明多个常量
const (
    STATUS = 10
    COUNT  = 100
)

iota概念

iota不是英文单词的缩写

希腊字母I的读音yota,英文单词的读音[aɪˈoʊtə]。

意思是微小的量

当批量声明常量的时候,第一个常量一定要赋值,后续没有初始化赋值的常量会自动赋值最后一个初始化赋值的常量的值。

package main

import "fmt"

func main() {
	const (
		a = 0
		b = 1  // 最后一个是这个,所以后续的变量都为1
		c
		d
		e
	)

	fmt.Println(a, b, c, d, e) // 0 1 1 1 1
}

如果想要进行自动累加

package main

import "fmt"

func main() {
	const (
		a = iota
		b
		c
		d
		e
	)

	fmt.Println(a, b, c, d, e) // 0 1 2 3 4 
}

特性

  1. 在一组const中,iota是在这一组中,无论是否使用iota作为值,iota都会自增1,初始值为0。
  2. 不同const中的iota互不影响
package main

import "fmt"

func main() {
	const (
		a = 0
		b = iota
		c
		d
		e
	)
	const (
		f = iota
		g
		h
		i
		j
	)
	fmt.Println(a, b, c, d, e, f, g, h, i, j) // 0 1 2 3 4 0 1 2 3 4
}

tips:

1、常量不能使用:=进行赋值操作

2、常量声明后不使用不会报错

3、常量声明必须初始化赋值

4、iota的值默认为0,每声明一个常量iota会自动+1

Identifiers 标识(zhì )符

标识符命名有以下几种

使用场景
camelCase驼峰命名法
PascalCase帕斯卡命名法 (大驼峰)
snake_case蛇形命名法
kebab_case中横线命名法一般文件夹名使用
space case空格命名法写代码用不到这种

Go语言能使用的标识符元素

  1. 大写字母
  2. 小写字母
  3. 数字
  4. 下划线

tips:除了下划线以外其他符号都不能用,和其他语言有点不同,只有这4个可以

Go命名规范

项目名:

  1. 使用中横线命名法
  2. 全小写

包名:

  1. 包名与目录保持一致
  2. 全部小写
  3. 不能使用下划线
  4. 不能用标准库的名称

模块名

全小写

使用蛇形命名法

常量:

  1. 全部大写字母+蛇形命名法

结构体

遵循变量命名规范

接口名

er后缀,遵循变量命名规范

函数名和普通变量

1、需要暴露到在包外的标识符都必须大写开头

2、不需要暴露到在包外的标识符都必须小写开头

数据类型

类型大纲

reflect.TypeOf()

Go语言数据类型分类

1. 基本类型

数字、字符串和布尔值(bool)

2. 复合类型

有8个类型,细分为

  • 聚合类型:数组(Array)和结构体(struct)、切片(slice)、映射(Map)
  • 指针类型:指针(Pointer)、
  • 函数类型:函数(func)
  • 通道类型:通道(Channel)
  • 接口类型:接口(interface)

按内存传递方式进行分类

  • 值类型: 布尔类型、数值类型、字符串类型、数组类型、结构体
  • 引用类型:指针、通道、切片、接口、Map、函数

值类型与引用类型区别

值类型引用类型
存储位置栈空间堆空间,栈中存储的是该对象在堆中的内存地址
复制方式值传递指针传递
生命周期所在代码块被销毁时一并销毁由垃圾回收机制控制
可变性不可变(不包括使用指针)可变

类型值范围:

Go源码:go.dev/src/builtin…

类型表:pkg.go.dev/builtin#pkg…

基本类型

数字类型

整型、浮点型、复数、指针

类型字节数二进制存储位数取值范围
int4/832/64-2^312^31-1 / -2^632^63-1
int818-2^72^7-1 (-128127)
int16216
int32432
int64864
float32432-3.4E+38~-1.4E-45
float64864

complex64:两个float32 => 一个表示32位实部,一个表示32位虚部

complex128:两个float64 => 一个表示64位实部,一个表示64位虚部

有符号的整数类型:

int、int8、int16、int32、int64、

无符号的整数类型:

uint、uint8、uint16、uint32、uint64、uintptr

uintptr

用来存储指针,系统是32位便是32位,是64位便是64位。

byte、rune

作用:用于存放字符编码的类型

rune是int32的别名,用来存储unicode码,www.geeksforgeeks.org/rune-in-gol…

byte是uint8的别名, 用来存储ASCII码。byte无法被类型推断出来。

为什么要出这个两个?

1、代码可读性

2、避免类型混淆与错误使用

使用场景

byte一般是在处理字节数据的时候使用。

rune一般是在处理Unicode字符串的时候使用

像'a'这样子的字符是数字来着,byte和rune都是类型别名

package main

import (
	"fmt"
)

func main() {
	data := []byte{0x41, 0x42, 0x43} // 定义一个字节切片
	// var b uint8 = data[0]            // 将第一个字节赋值给变量b
	var b byte = data[0]

	if b == 'A' { // 如果b等于'A'字符的ASCII码
		fmt.Println("Match") // 输出匹配
	}

	str := "Hello, 世界"
	for _, r := range str { // 遍历字符串中的每个Unicode字符
		fmt.Printf("%c", r)
	}
}

数值转换

负数转无符号整型
package main

import (
	"fmt"
)

func main() {
	var a int8 = -123
	var b uint8 = uint8(a) // 256 - 123
	fmt.Println(b) // 133    
}
小数转整型
package main

import (
	"fmt"
)

func main() {
	var a = 3.14
	var b = int32(a)
	fmt.Println(b) // 3  舍弃小数
} 

需要注意点

  1. 在32位系统的时候,int是int32,在64位系统的时候,int是int64
  2. 虽然int与int32/int64范围可能是一样的,但是两者类型并不相等,也就是int与int32/int64不能直接进行相加,需要先做显示转换成相同类型。
  3. 浮点型默认类型是float64

字符串类型string

1、存储字符集合的类型

2、值用双引号

3、在Go语言中,中文占3个字节,因为Go默认使用UTF-8编码

package main

import (
	"fmt"
)

func main() {
	str := "你好,世界"
	fmt.Println(len(str)) // 15
}

字符串转换

package main

import (
	"fmt"
	"strconv"
)

func main() {

	var str = "abc"
	// a => string
	// to => 转换
	// i => int
	// 字符串其他类型可能会失败
	a, err := strconv.Atoi(str)
	fmt.Println("输出", a, err) //输出 0 strconv.Atoi: parsing "abc": invalid syntax
	//a是0是因为int默认值是0
}
package main

import (
	"fmt"
	"strconv"
)

func m ain() {
	var a = 123
	str := strconv.Itoa(a)
	fmt.Println("输出:", str)
}
package main

import (
	"fmt"
	"strconv"
)

func main() {
	// 参数1是字符串,参数2是32位还是64位,返回的是float64类型
	a, err := strconv.ParseFloat("3.1415", 32)
	if err != nil {
		fmt.Println("输出:", err)
	}

	fmt.Println(a)
}
package main

import (
	"fmt"
	"strconv"
)

func main() {
    // 其他进制转10进制
	// 参数1是字符串,参数2是字符串的进制。 参数3是限制最大位数
	// 需要进制转换就可以用这个
	a, err := strconv.ParseInt("100", 2, 64)
	if err != nil {
		fmt.Println("输出:", err)
	}

	fmt.Println(a)
}
package main

import (
	"fmt"
	"strconv"
)

func main() {
	a, err := strconv.ParseBool("0")  // 除了0、1、true、false以外都会报错。
	if err != nil {
		fmt.Println("输出:", err)
	}

	fmt.Println(a)
}
package main

import (
	"fmt"
	"strconv"
)

func main() {
	a := strconv.FormatBool(true)
	fmt.Println(a) // true
}
package main

import (
	"fmt"
	"strconv"
)

func main() {
    // 参数1:float64 参数2: 格式 参数3:精度,-1的话是参数的小数点位数  参数4:转换为多少位
	a := strconv.FormatFloat(3.14159265, 'f', 2, 64)
	fmt.Println(a)
}
package main

import (
	"fmt"
	"strconv"
)

func main() {
	// 十进制100转成2进制并转化成字符串
	a := strconv.FormatInt(100, 2)
	fmt.Println(a)
}
package main

import "fmt"

func main() {
	a := "I love you!"
	aBytes := []byte(a) // []切片, 每一项是byte. 意思就是把a转为每个元素为byte类型的切片
	fmt.Println(aBytes) // [73 32 108 111 118 101 32 121 111 117 33]
	fmt.Println(len(aBytes)) // 11

	b := "我爱你"
	bByte := []byte(b)
    bRunes := []rune(b)
	fmt.Println(bByte) //[230 136 145 231 136 177 228 189 160]
	fmt.Println(len(bByte)) // 9
    fmt.Println(bRunes) // [25105 29233 20320]
	fmt.Println(len(bRunes)) // 3
}

获取长度

内置方法:len() 获取字符串字节长度

array 数组

数组是编程语言中,最常见的有序列表的数据结构

特点:

  1. 数组的有序性:由索引描述数组中元素的位置
  2. 数组的可遍历性:通过有序的特征,可以将元素按照索引顺序进行迭代
  3. 数组定长性: 数组在声明定义时,就必须指定长度
  4. 数组的类型确定性: 数组只能存放定义时指定的类型的元素

相等性判断

  1. 数组的元素类型要一致
  2. 数组的元素长度一致
  3. 数组元素一一对应,值一样
package main

import (
	"fmt"
)

func main() {
	var arr1 [3]string
	var arr2 [4]string

	fmt.Printf("%T\r\n", arr1) // [3]string
	fmt.Printf("%T\r\n", arr2) // [4]string
	// fmt.Println(arr1 == arr2) //报错invalid operation: arr1 == arr2 (mismatched types [3]string and [4]string)compilerMismatchedTypes
	var arr3 [3]string
	var arr4 [3]string
	fmt.Println(arr3 == arr4) //true
}

数组初始化

var arr [3]string
arr := [3]string{"张三","李四","王五"}
arr := [3]string{1: "张三", 2: "王五"}
arr := [...]string{"张三", "李四", "王五"}
// 多维数组
var arr[3][5]string //定义一个3行五列的二位数组

关于...一个有趣的用法

package main

import "fmt"

func main() {
	arr := [...]int{99: 0}  // 定义第99项为0
	fmt.Println(arr) // [0 0 0 ... 0 0 0 0 0]
	fmt.Println(len(arr))  // 100
}

截取

package main

import "fmt"

func main() {
	arr1 := [3]int{1, 2, 3}
	slice1 := arr1[1:2] // 创建切片
	fmt.Println(arr1, slice1) // [1 2 3] [2]
	slice1[0] = 5
	fmt.Println(arr1, slice1) // [1 5 3] [5]  
}

结构体的JSON转化

omitempty的意思是

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	ID         int
	FirstName  string `json:"name"`
	LastName   string
	Address    string      `json:"address,omitempty"`
	Coordinate *coordinate `json:"coordinate,omitempty"`
}

type Employee struct {
	Person
	ManagerID int
}

type coordinate struct {
	Lat *float64 `json:"latitude,omitempty"`
	Lng *float64 `json:"longitude,omitempty"`
}

func main() {
	employees := []Employee{
		Employee{
			Person: Person{
				ID:        1,
				LastName:  "Last1名字",
				FirstName: "First1名字",
				Address:   "地址1",
			},
		},
		Employee{
			Person: Person{
				ID:        2,
				LastName:  "Last2名字",
				FirstName: "First2名字",
				Address:   "地址2",
			},
		},
	}

	data, _ := json.Marshal(employees)
	fmt.Printf("%s\n", data)

	var decoded []Employee
	json.Unmarshal(data, &decoded)
	fmt.Printf("%v", decoded)
}

等学了指针在回头回来更新这个
zhuanlan.zhihu.com/p/470360844…

派生类型

切片(slice)

  1. 本质是一个基于数组实现的动态数组,即数组的长度是自动扩容。
  2. 不能进行相等性判断,只能与nil进行比较。
  3. 切片需要使用append方法进行元素添加,append方法是全局方法,并返回新的引用,必须赋值给一个变量,只能用在默认长度0的数组,即[0]string也不行。

切片初始化

  1. 默认长度为0的数组
  2. 定义切片的初始化
  3. make方法初始化存储空间
var slice1 []string
	arr := append(slice1, "张三", "李四", "王五")
	slice1 := []string{"张三", "李四", "王五"}
arr := make([]string, 2) // 初始化定义2个空间的切片
//如果事先知道需要存储多少个元素,就可以使用make来指定多少空间
//使用mask的好处: 减少扩容次数。
// make([]Type, length, cap)
// cap其实可以说是预留空间,如果只用len进行扩容的话,每一次扩容都是把实际容量开辟翻倍,具体看添加那一块的tips

切片操作

访问
package main

import "fmt"

func main() {
	arr := []string{"张三", "李四", "王五"}
	fmt.Println(arr[1]) // 李四
}
截取(左闭右开)
package main

import "fmt"

func main() {
	arr := []string{"张三", "李四", "王五", "赵六"}
	fmt.Println(arr[1:3]) // [李四 王五]
	fmt.Println(arr[1:]) // [李四 王五 赵六]
    fmt.Println(arr[1 : len(arr)-2]) // [李四]
    fmt.Println(arr[:]) // [张三 李四 王五 赵六]
}

tips:截取出来是一个新的切片,不过切片的底层array还是指向同一个(部分),所以修改截取的值一样会修改到原本的。

type slice struct {

array 0x001

len 5

cap 5

}

左边旧的slice

右边新的,截取[1:3]

type slice struct {

array 0x002

len 2

cap 2

}

添加元素(省略号)

也可以用循环 ,省略号这个类似于js的扩展运算符

package main

import "fmt"

func main() {
	arr := []string{"张三", "李四", "王五", "赵六"}
	arr1 := []string{"测试", "测试1", "测试2"}
	arr3 := append(arr, arr1...) 
	arr4 := append(arr, arr1[1:]...)
	fmt.Println(arr3) // [张三 李四 王五 赵六 测试 测试1 测试2]
	fmt.Println(arr4) // [张三 李四 王五 赵六 测试1 测试2]
}
删除

go里其实没有删除,应该叫排除。

package main

import "fmt"

func main() {
	arr := []string{"张三", "李四", "王五", "赵六"}
	arr = arr[1:]
	fmt.Println(arr) // [李四 王五 赵六]
}
复制
引用赋值(底层并非完全是引用赋值)
package main

import "fmt"

func main() {
	arr := []string{"张三", "李四", "王五", "赵六"}
	newArr := arr
	newArr[0] = "测试"
	fmt.Println(arr) //[测试 李四 王五 赵六] 原本的arr会被改变
	newArr = append(newArr, "4")
	// 实际arr并没有扩容
	fmt.Println(arr, newArr) //[测试 李四 王五 赵六] [测试 李四 王五 赵六 4]
}

//赋值其实是拷贝了下面这个。 
// arr是引用,但是len和cap不是,修改不了原本的len和cap,所以只能对array进行操作
// go源码slice的定义就是下面这个
type slice struct {
	array unsafe.Pointer   // 连续空间首地址
	len   int
	cap   int
}

使用copy进行拷贝
package main

import "fmt"

func main() {
	arr := []string{"张三", "李四", "王五", "赵六"}
	newArr := make([]string, len(arr))
	res := copy(newArr, arr)
	fmt.Println(res) //4  返回拷贝切片的长度
	newArr[0] = "测试"
	fmt.Println(arr, newArr) // [张三 李四 王五 六] [测试 李四 王五 赵六]
	//不会改变原有的arr
}

tip: 栈中只保存了指向堆中数组(或其他动态分配内存的对象)的首地址,而堆的连续地址是由操作系统的内存管理机制通过页表和地址空间来区分不同的内存块。

切片扩容

go源码里注释写着小切片2倍扩容,大切片1.25倍扩容,代码里以256为分界线,不同go版本扩容的倍速不一样,具体看。计算出来与实际点小小的偏差。留着后面在研究,貌似是mallocgc的原因导致的。

  1. 在未定义容量时候,容量与长度相等。
  2. 如果append的过程中没有引起了扩容,将返回原本的切片引用
  3. 如果append的过程中引起了扩容,那么将重新分配内存空间,存储元素,并返回新的切片对象
go version go1.21.4 windows/amd64
for 0 < newcap && newcap < newLen {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
}
package main
import (
	"fmt"
	"strconv"
)
func main() {
	arr := make([]string, 0)
	for i := 1; i < 850; i++ {
		arr = append(arr, strconv.Itoa(i))
		fmt.Println("arr", len(arr), cap(arr))
	}
}
/*
    arr [1] 1 1
	arr [1 2] 2 2
	arr [1 2 3] 3 4
	arr [1 2 3 4] 4 4
	arr [1 2 3 4 5] 5 8
	arr [1 2 3 4 5 6] 6 8
	arr [1 2 3 4 5 6 7] 7 8
	arr [1 2 3 4 5 6 7 8] 8 8
	arr [1 2 3 4 5 6 7 8 9] 9 16
    ...
    arr 459 512
    ...
    arr 813 848
	...
    arr 849 1280
*/
package main

import (
	"fmt"
)

func main() {
	intSlice := make([]int, 3, 5)
	newIntSlice := append(intSlice, 4)
	fmt.Printf("%v, %p, %d, %d\r\n", intSlice, &intSlice[0], len(intSlice), cap(intSlice))             
	fmt.Printf("%v, %p, %d, %d\r\n", newIntSlice, &newIntSlice[0], len(newIntSlice), cap(newIntSlice)) 
    // [0 0 0], 0xc0000103f0, 3, 5
    // [0 0 0 4], 0xc0000103f0, 4, 5
}
package main
import (
	"fmt"
)
func main() {
	intSlice := []int{1, 2, 3}
	newIntSlice := append(intSlice, 4)
    //首地址不一样
	fmt.Printf("%v, %p, %d, %d\r\n", intSlice, &intSlice[0], len(intSlice), cap(intSlice))  //[1 2 3], 0xc00000e108, 3, 3
	fmt.Printf("%v, %p, %d, %d\r\n", newIntSlice, &newIntSlice[0], len(newIntSlice), cap(newIntSlice)) //[1 2 3 4], 0xc0000103f0, 4, 6
}
思考:

扩容为什么要重新开辟连续空间来分配?

因为原有的连续空间不能保证后续的预空间没有被占用。比如2个容量的slice占了0x001、0x002,扩容的时候0x003有可能是占用了的,所以Go选择重新开辟一个预空间的连续空间来达到扩容目的。

Map

定义: 一种包含多个key-value键值对的集合

特点:

  1. 无序列表集合
  2. key-value结构
  3. key具有唯一性
  4. 读、写、删效率相对比较高
  5. 不能进行比较运算的类型不能做key,因为key在系统插入中要对比重复 ,例如 slice、map、func都不能作为key
  6. 会自动扩容,不需要使用append

初始化

package main

import "fmt"

func main() {
	//第一种
	// [key type]value type
	myInfo := map[string]string{}
	myInfo["name"] = "zx"
	myInfo["sex"] = "男"
	//第二种
	sex := "sex"
	myInfo1 := map[string]string{
		"name": "zx",
		sex:    "男", // 最后一个key:value后面也需要写逗号
	}
	// 第三种
	// 参数1:map类型的数据
	// 参数2:容量(可选)
	myInfo2 := make(map[string]string)
	fmt.Println(myInfo)
	fmt.Println(myInfo1)
	fmt.Println(myInfo2)

}

判断key是否存在

package main

import "fmt"

func main() {
	sex := "sex"
	myInfo1 := map[string]string{
		"name": "zx",
		sex:    "男", // 最后一个key:value后面也需要写逗号
	}
	val, exist := myInfo1["name"]
	val1, exist1 := myInfo1["age"]
	fmt.Println(val, exist)   // zx true
	fmt.Println(val1, exist1) //  false
}

删除

package main

import "fmt"

func main() {
	sex := "sex"
	myInfo1 := map[string]string{
		"name": "zx",
		sex:    "男", // 最后一个key:value后面也需要写逗号
	}
	fmt.Println(myInfo1) // map[name:zx sex:男]
	delete(myInfo1, "name")
	fmt.Println(myInfo1) // map[sex:男]
	delete(myInfo1, "xxxxxx")
	fmt.Println(myInfo1) // map[sex:男]
}

枚举

package main

import "fmt"

func main() {
	sex := "sex"
	myInfo1 := map[string]string{
		"name": "zx",
		"age":  "24",
		sex:    "男", 
	}
	for key, value := range myInfo1 {
		fmt.Println(key, value)
	}
	// for range对map的枚举是无序的,每次打印出来的顺序都不一样,因为map是无序的集合
}

多类型value

package main

import "fmt"

func main() {
	map1 := map[string]interface{}{
		"name": "zx",
		"age":  24,
	}
	fmt.Println(map1) // map[age:24 name:zx]
}

hash与键值对的存储原理

1、什么是哈希(Hash)?

哈希(Hash)是一种将任意长度的消息压缩到某一固定长度的消息摘要(Message Digest)的函数。

{"name": "zx"}

将"name"通过hash算法(createHash(key))生成一串由字母和数字组成的一串字符,这串字符就是hash

  1. 相同的key生成的hash是一样的 (hash的唯一性)
  2. 理论上无穷大的key都可以通过哈希算法生成一个一定大的hash值(hash的压缩性)
  3. 映射就是key与hash之间的对应关系 (hash的映射性)
  4. 不同的hash值对应的数据存储在不同的空间内 (hash的离散性)
  5. hash算法不能把hash值反推出key值 (hash的不可逆性) (因为第2点,压缩性,大转小肯定是会丢失一些信息。)
  6. 由于生成hash长度是固定的,两个key可能会得到相同的hash。(hash的冲突性)

bucket array

将通过hash算法将key生成hash,在通过离散算法得到存储在bucket array的位置,将hash存入到该桶的链表中。

渐进式迁移

在map扩容的时候,不会立刻把旧的bucket array数据迁移到新的bucket array,而是在执行写入的操作的迁移写写入的这个key.

例如{name: "zx", age: 24},现在我添加sex:"男"进去发生了扩容,那么sex会在新的bucket array里。而name和age只有等写入的时候才会把name、age迁移过去。例如我现在修改了map["name"] = "wzx"。那么name就会迁移到这个新的bucket array,而age并没有发生写入操作,所以还在原来的bucket array。

指针

什么是指针?

指针在Go语言中是一种存放内存空间地址的类型

指针总是会指向地址对应的空间

在Go语言中,指针可以直接访问其内部的属性。

指针在Go语言不能直接参与运算,可以使用unsafe.Pointer来使指针参与运算。

指针的基本使用

指针类型 : *普通数据类型 *int *bool

指针值:&普通数据变量名 &a &b

指针获取值:*指针变量名 *a *b

指针的作用

  1. 动态分配内存
  2. 进行指针的传递。 因为如果把整个引用值都传递,可能会很大,而传递这个值的指针就可以解决这个问题。
  3. 修改外部变量值或者是其内部的变量值,例如: 更改函数参数值(指针)
package main

import (
    "fmt"
)

func main() {
    var x int = 1
    var ptr *int = &x
    *ptr = 100
    fmt.Println(x)  // 100
}

new方法

参数: 类型。例: new(int)

作用:分配一个内存空间 ,返回指针类型的值,不是野指针。

package main

import "fmt"

func main() {
	a := new(int)
	*a = 100
	fmt.Println(*a) // 100
	b := 200
	a = &b
	fmt.Println(*a) // 200
	b = 300
	fmt.Println(*a) // 300
}

指针与变量、指针初始化的问题

package main

import "fmt"

func exchange(x *int, y *int) {
	x, y = y, x
	fmt.Println(*x, *y) // 2 1
	fmt.Println(x, y)   // 0xc000096080 0xc000096068
}

func main() {
	x := 1
	y := 2
	exchange(&x, &y)
	fmt.Println(x, y)   // 1 2
	fmt.Println(&x, &y) // 0xc000096068 0xc000096080
}

上面的代码,内部的x、y的指针交换了,但是外面没换,因为他两也是变量来着,交换的是内部的x、y的指针。

即原本函数的x指向main的x,函数的y指向main的y,但是现在内部换了,就变成了函数的x指向main的y,函数的y指向main的x。

解决这个问题直接交换值,而不是交换指针。

package main

import "fmt"

func exchange(x *int, y *int) {
	*x, *y = *y, *x
	fmt.Println(*x, *y) // 2 1
	fmt.Println(x, y)   // 0xc00000a0b8 0xc00000a0d0
}

func main() {
	x := 1
	y := 2
	exchange(&x, &y)
	fmt.Println(x, y)   // 2 1
	fmt.Println(&x, &y) // 0xc00000a0b8 0xc00000a0d0
}
野指针
  • 指不明确指向任何空间,即只声明没有初始化的指针。
  • 值为nil,指向0x0
package main
import "fmt"
func main() {
	var a *int          // 只定义并且未初始化
	fmt.Println(a)      // <nil>
	fmt.Printf("%p", a) // 0x0
}

自定义类型

定义一个新的类型

自定义类型是一种新的类型,与内置类型是不同类型,也就是与原本的不能进行比较,别名可以。

作用: 隔离运算。例如你有账单,你可以定义个类型给账单里的数字,这样就可以产生只能账单里的数据进行运算,非账单数据与账单的数据就不能运算的效果。

type MyInt int

定义类型别名

作用:

  1. 增加语义化
  2. 简洁变量类型,比如函数传递回调参数的时候看着就蛋疼的返回函数的函数
type Myint = int
type Callback = func(a int, b int, sign string, res int) string
	compute := func(a, b int, method string, cd Callback) string {}
  1. reflect.TypeOf(variable)
  2. reflect.ValueOf(variable).Kind()
package main

import (
	"fmt"
	"reflect"
)

func main() {

	type typeAmount float64
	var a typeAmount = typeAmount(11111.1111)
	var b = 123123.13123
	var c = a + typeAmount(b)
	cType := reflect.TypeOf(c)          // main.typeAmount
	ccType := reflect.ValueOf(c).Kind() // float64
	fmt.Println(cType, ccType)

}

tips: typeAmount和float64不能直接运算,需要进行显示化

什么时候使用类型别名,什么时候使用自定义类型

如果只是为了语义化类型定义,或者短化类型定义就选类型别名

如果定义的类型是复合类型、多元素类型、复杂的,就用自定义类型

类型方法

给自定义类型添加方法

作用:

增强了方法的集成性,并且防止方法重名。

语法格式

将方法(methodName)添加到自定义类型(Type)下

func (variableName Type) methodName(parametersName) returnType {
     // 方法体
 }

练手

通过(cs ComputeSlice)给所有的ComputeSlice类型的所有数据提供加减乘除的方法,所有该类型的数据都可以直接调用这些方法

package main

import "fmt"

func main() {
	var slice ComputeSlice = []int{1, 2}
	fmt.Println(slice.Plus())
	fmt.Println(slice.Minus())
	fmt.Println(slice.Multiply())
	fmt.Println(slice.Division())

}

type ComputeSlice []int

func (cs ComputeSlice) Plus() int {
	return cs[0] + cs[1]
}

func (cs ComputeSlice) Minus() int {
	return cs[0] - cs[1]
}

func (cs ComputeSlice) Multiply() int {
	return cs[0] * cs[1]
}

func (cs ComputeSlice) Division() int {
	return cs[0] / cs[1]
}

// func Plus(a, b int) int {   // 并不会报错,自定义方法和全局方法不在同个地方
// 	return a + b
// }

// 3
// -1
// 2
// 0

优化一下

package main

import "fmt"

func main() {
	var slice ComputeSlice = []int{1, 2}
	fmt.Println(slice.compute("PLUS"))
	fmt.Println(slice.compute("MINUS"))
	fmt.Println(slice.compute("MULTIPLY"))
	fmt.Println(slice.compute("DIVISION"))

}

type ComputeSlice []int

func (cs ComputeSlice) compute(method string) int {
	switch method {
	case "PLUS":
		return plus(cs[0], cs[1])
	case "MINUS":
		return Minus(cs[0], cs[1])
	case "MULTIPLY":
		return Multiply(cs[0], cs[1])
	case "DIVISION":
		return Division(cs[0], cs[1])
	default:
		return 0
	}
}
func plus(a, b int) int {
	return a + b
}

func Minus(a, b int) int {
	return a - b
}

func Multiply(a, b int) int {
	return a * b
}

func Division(a, b int) int {
	return a / b
}

// 3
// -1
// 2
// 0

零值

零值是编译器自动为未初始化变量设置的默认值,

与默认值的区别:默认值是在某些情况下为变量提供的预定义值。

类型零值
int、int8、int16....0
float32、float64+0.000000e+000
boolfalse
string""
pointer、channel、func、interface、slice、mapnil

struct严格上说没有零值,struct是数据的结构,内部有结构的装载具体类型,当然内部的字段的零值就还是对应类型的零值。

package main

import "fmt"

type Person struct{}
type Person1 struct {
	age int
}

func main() {
	type I interface{}
	var b bool
	var n int
	var f32 float32
	var f64 float64
	var s string
	var p *int
	var sl []int
	var m map[string]string
	var c chan int
	var f func()
	var i I
	var structTest Person
	var structTest1 Person1
    //打印结果,第六个因为是空字符,所以看不出来
	fmt.Println(b, n, f32, f64, s, p, sl, m, c, f, i, structTest, structTest1) // false 0 0 0  <nil> [] map[] <nil> <nil> <nil> {} {0}

}

特别注意,虽然slice和map打印出来不是nil,但是其实它是零值来着。

package main
import "fmt"
func main() {
	var sl []int           // 声明但未初始化的切片变量
	fmt.Println(sl == nil) // 输出true
	fmt.Println(sl)        // 输出[],也叫nil切片

	var m map[string]string

	fmt.Println(m == nil) // 输出true
	fmt.Println(m)        // 输出map[],也叫nil 映射
}

特别注意:打印出来[]和map[],虽然与空切片和空映射打印出来一样,但它们不是空切片、空映射。他们之间的区别是底层有没有分配资源,参考

  • nil切片表示一个未分配底层数组的切片
  • nil映射表示一个未分配底层数据的映射

特别的值 nil

  • nil是go语言中的零值,也叫空值,是引用类型的空值。
  • 基本类型没有nil,所以nil不能赋值给基本类型。
  • nil不是数据类型,不是关键字,是Go的标识符。
  • nil不可以使用:=进行初始赋值,因为nil对应很多类型,无法进行类型推断
  • nil与nil不能做比较,但是可以与引用类型变量进行比较,是类型的问题。

比较特别的打印

fmt.Printf("%T", nil) // 输出<nil>

虽然是%T,但是打印出的意思是没有类型。

链表与array比较

数组与链表都是有序列表

数组:

需要开辟一个连续空间

查询是采用顺序位偏移的方式,性能效率高

插入,特别是中间插入的时候,需要对后续元素进行移动,性能效率较低

链表:

不需要开辟连续空间,注入一个元素,开辟一个空间。

插入只需要把前后节点的指针进行重新指向,不需要对其他元素进行存储移动,性能效率相对较高

查询,需要遍历元素进行查询,性能效率较低。

list

type List struct {
	root Element // sentinel list element, only &root, root.prev, and root.next are used
	len  int     // current list length excluding (this) sentinel element
}
// Element is an element of a linked list.
type Element struct {
	// Next and previous pointers in the doubly-linked list of elements.
	// To simplify the implementation, internally a list l is implemented
	// as a ring, such that &l.root is both the next element of the last
	// list element (l.Back()) and the previous element of the first list
	// element (l.Front()).
	next, prev *Element

	// The list to which this element belongs.
	list *List

	// The value stored with this element.
	Value any
}

声明、循环

package main

import (
	"container/list"
	"fmt"
)

func main() {
	// 三种声明
	// var linkList list.List
	// var linkList = new(list.List).Init()
	var linkList = list.New()

	linkList.PushBack("Golang") // 最后加
	linkList.PushFront("Java")  // 最前加
	linkList.PushBack("Rust")

	fmt.Println(linkList)
	fmt.Println(linkList.Len())
	fmt.Println(linkList.Front())
	//正序
	for i := linkList.Front(); i != nil; i = i.Next() {
		fmt.Println(i)
	}
	// 倒序
	for i := linkList.Back(); i != nil; i = i.Prev() {
		fmt.Println(i)
	}

}

操作方法

PushBack、PushFront、InsertBefore、InsertAfter

InsertBefore 方法是将一个新的元素插入到指定元素之前。它接收两个参数,第一个参数是要插入的元素的指针,第二个参数是目标元素的指针,将新元素插入到目标元素之前。如果第二个参数为 nil,则会将新元素插入到列表的末尾。

package main

import (
	"container/list"
	"fmt"
)

func main() {
	//初始化一个链表(返回一个初始化完毕的链表)
	var linkList = list.New()

	//
	golangNote := linkList.PushBack("Golang") //在链表最后新增。返回新增的元素。参数是Value,PushBack会把Value包装成Element对象
	javaNote := linkList.PushFront("Java")    // 在链表开头新增。返回新增的元素
	linkList.PushBack("Javascript")

	fmt.Println(golangNote)       // &{0xc00001e0f0 0xc00001e0f0 0xc00001e0f0 Golang}
	fmt.Println(golangNote.Value) // Golang

	//插入
	linkList.InsertBefore("Python", javaNote) // 在Element之前加入
	linkList.InsertAfter("PHP", javaNote)     // 在Element之后加入

	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e)
		/*
		 * &{0xc00001e150 0xc00001e0f0 0xc00001e0f0 Python}
		 * &{0xc00001e270 0xc00001e240 0xc00001e0f0 Java}
		 * &{0xc00001e120 0xc00001e150 0xc00001e0f0 PHP}
		 * &{0xc00001e180 0xc00001e270 0xc00001e0f0 Golang}
		 * &{0xc00001e0f0 0xc00001e120 0xc00001e0f0 Javascript}
		 **/
	}
}

插入(但是没有新增返回的情况)

package main

import (
	"container/list"
	"fmt"
)

func main() {

	var linkList = list.New()
	linkList.PushBack("Golang")
	linkList.PushBack("Java")
	linkList.PushBack("Javascript")

	e := linkList.Front()
	//只能用for找到Element
	for ; e != nil; e = e.Next() {
		if e.Value.(string) == "Java" {
			break
		}
	}

	linkList.InsertBefore("Python", e) // 在Element之前加入
	linkList.InsertAfter("PHP", e)     // 在Element之后加入
	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e)
		/*
		 * &{0xc00001e1b0 0xc00001e0f0 0xc00001e0f0 Golang}
		 * &{0xc00001e150 0xc00001e120 0xc00001e0f0 Python}
		 * &{0xc00001e1e0 0xc00001e1b0 0xc00001e0f0 Java}
		 * &{0xc00001e180 0xc00001e150 0xc00001e0f0 PHP}
		 * &{0xc00001e0f0 0xc00001e1e0 0xc00001e0f0 Javascript}
		 **/
	}
}
MoveBefore、MoveAfter

将一个已存在的元素移动到指定元素之前。

它接收两个参数,第一个参数是要移动的元素的指针,第二个参数是目标元素的指针,将移动元素插入到目标元素之前。如果第二个参数为 nil,则会将元素移动到列表的末尾。

package main

import (
	"container/list"
	"fmt"
)

func main() {
	// 初始化列表
	linkList := list.New()

	// 添加元素
	linkList.PushBack("Golang")
	linkList.PushBack("Java")
	linkList.PushBack("Javascript")

	// 打印列表
	fmt.Println("移动前:")
	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value)
	}
	movingElement := linkList.Front()
	targetElement := linkList.Front()
	// 把 "b" 元素移动到列表头部之前
	for e := linkList.Front(); e != nil; e = e.Next() {
		if e.Value.(string) == "Java" {
			targetElement = e
		}

		if e.Value.(string) == "Javascript" {
			movingElement = e
		}
	}
	linkList.MoveBefore(movingElement, targetElement)
	// linkList.MoveAfter(movingElement, targetElement) // 移动回来

	// 打印移动后的列表
	fmt.Println("移动后:")
	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value)
	}
}

// 移动前:
// Golang
// Java
// Javascript
// 移动后:
// Golang
// Javascript
// Java
InsertBefore和MoveBefore的区别

MoveBefore 方法操作的是已经存在于列表中的元素,而 InsertBefore 方法则是插入一个新元素。

MoveToFront、MoveToBack

  • MoveToFront将元素移动到最前面
  • MoveToBack将元素移动到最后面
package main

import (
	"container/list"
	"fmt"
)

func main() {
	var linkList = list.New()
	linkList.PushBack("Golang")
	linkList.PushFront("Java")
	Javascript := linkList.PushBack("Javascript")
	linkList.MoveToFront(Javascript) // 将Javascript移动到最前面
	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value)
	}
}

// Javascript
// Java
// Golang

合并链表

PushBackList 合并到后面

PushFrontList 合并到前面

package main

import (
	"container/list"
	"fmt"
)

func main() {
	var linkList = list.New()
	linkList.PushBack("Golang")
	linkList.PushBack("Java")
	linkList.PushBack("Javascript")

	var linkList1 = list.New()
	linkList1.PushBack("1")
	linkList1.PushBack("2")
	linkList1.PushBack("3")

	// linkList.PushBackList(linkList1) // 将linkList1合并到linkList后面
	linkList.PushFrontList(linkList1) // 将linkList1合并到linkList前面

	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value)
	}
}

删除

Remove

package main

import (
	"container/list"
	"fmt"
)

func main() {
	var linkList = list.New()
	linkList.PushBack("Golang")
	javaNode := linkList.PushBack("Java")
	linkList.PushBack("Javascript")

	linkList.Remove(javaNode) // 将linkList1合并到linkList前面

	for e := linkList.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value)
	}
}

类型转换

有两种转换方式

使用内置函数

package main

import (
	"fmt"
)

func main() {
	var integer16 int16 = 127
	var integer32 int32 = 32767
	fmt.Println(int32(integer16) + integer32)
}

使用 strconv 包

string conversion

vers 转换

golang.org/pkg/strconv…

package main

import (
	"fmt"
	"strconv"
)

func main() {
	i, _ := strconv.Atoi("-42")
	s := strconv.Itoa(-42)
	fmt.Println(i, s)
}

算数运算

  1. 在算数运算中,Go没有隐式类型转换,必须使用类型转换统一类型才能进行算数运算
  2. 不合理运算在Go都是不能进行的,比如字符串减法、布尔值运算
package main

import (
	"fmt"
)

func main() {
	var a byte = 'a'
	var b byte = 'b'
	res := a - b
	fmt.Println(a, b, res)  // 97 98 255 
    // 因为是byte,但值是-1,所以 255 - 1 = 255
}
package main

import "fmt"

func main() {
	a := 1
	b := 0
	res := a / b
	fmt.Println(res)  //在运行时会报错, runtime error: integer divide by zero
}
//当时被除数是0.0的时候
func main() {
	a := 1.0
	b := 0.0
	res := a / b
	fmt.Println(res) //+Inf  因为0.0是浮点数,并不精确,只能说他无限接近于0
}

//当时被除数是0.0的时候
func main() {
	a := 0.0
	b := 0.0
	res := a / b
	fmt.Println(res) //NaN    tip:NaN可以使用math.NaN()输出标识符NaN,NaN不等于任何值,包括自己
}

关系运算

只有类型相等的才能进行运算

、<、>=、<=、==、!=

自增自减

  1. 在Go中的++、--与JS不一样,在Go语言中++、-- 不是表达式,是语句。
  2. 不能参与运算或赋值,只能单独出现。
  3. ++只能写在变量后,不能写成++a,只能a++
package main

import (
	"fmt"
)

func main() {
	a := 1
	a++
	// b := a++  不能参与运算
	// ++a 不能写在前面
	fmt.Println(a)
}

格式化输出

Println (全称:print line ),Print

package main

import "fmt"

func main() {
	fmt.Println("aaa") //自带回车换行
	fmt.Println("bbb")
	fmt.Print("aaa") // 不带回车换行
	fmt.Print("bbb")
}
// 最终结果
/*
 * aaa
 * bbb
 * aaabbb
 */

Printf (全称:print format), Sprintf

print性能比printf好

package main

import (
	"fmt"
	"strconv"
)

func main() {
	name := "xin"
	age := 24
	fmt.Print("My name is" + name + ", I am" + strconv.Itoa(age) + "years old.\r\n")
	fmt.Printf("My name is %s. I am %d years old.\r\n", name, age)
	res := fmt.Sprintf("My name is %s. I am %d years old.\r\n", name, age) // 会return出结果
	fmt.Println(res)

	fmt.Printf("Age类型%T\r\n", age)
	str := "abc"
	strRunes := []rune(str)
	fmt.Printf("%#v", strRunes)
}

// My name isxin, I am24years old.
// My name is xin. I am 24 years old.
// My name is xin. I am 24 years old.

// Age类型int
// []int32{97, 98, 99}

strings.Builder

strings 是字符串方法包,性能非常高


package mai
import (
	"fmt"
	"strconv"
	"strings"
)
func main() {
	name := "xin"
	age := 24
	var stringBuilder strings.Builder
	stringBuilder.WriteString("My name is")
	stringBuilder.WriteString(name)
	stringBuilder.WriteString(". ")
	stringBuilder.WriteString("I am ")
	stringBuilder.WriteString(strconv.Itoa(age))
	stringBuilder.WriteString(" years old.\r\n")
	res2 := stringBuilder.String()
	fmt.Println(res2)
}

一等公民 - 函数

go函数和javascript的函数很像。

1、使用func进行声明, 全称: function

2、函数名称 : 需要公共调用的方法命名需要大驼峰,否则Go不会导出该方法

3、类型定义: Go语言中,类型定义放在变量后面

4、返回值类型定义:在参数括号和后面

5、在函数内,函数作用域嵌套函数只能定义函数表达式,不能进行声明函数。Go特有的

声明方式:

1、具名函数声明

只能出现在全局作用域,函数内部不能进行具名函数声明

func test(){}

2、函数表达式

匿名函数声明赋值给一个变量的表达式

var test = func(){}

函数类型

  1. 基本形式: func ()

  2. 带参形式: func (a int, b int){}

  3. 带返回值的形式参数:func () int{} 和func() func()

看着就蛋疼的返回函数的函数

func test2(a int, b int) func(aa int, bb int) func(aaa int, bbb int) {
	return func(aa int, bb int) func(aaa int, bbb int) {
		return func(aaa int, bbb int) {

		}
	}
}
func test3(cb1 func(a int), cb2 func(b int)) func(aa int, bb int) func(aaa int, bbb int) {
	return func(aa int, bb int) func(aaa int, bbb int) {
		cb1(1)
		return func(aaa int, bbb int) {
			cb2(2)
		}
	}
}

特征:

1、函数可以作为参数进行传递(回调特性)

2、函数可以作为返回值抛出(闭包特性)

3、函数可以作为值进行变量赋值(函数值特性)

4、函数可以实现接口(满足接口特性)

import "fmt"
import "test/calculator"

import (
    "fmt"
    "test/calculator"
)

func Plus(a int, b int) int {
    return a + b
}

//变量复制
var result1 int = Plus(1, 2)
等价于
var result1 := Plus(1, 2) // 根据函数返回类型自动推断
func test1(cd func()){}
test1(func(){
    fmt.Println("匿名函数执行")
})

main 函数

main函数是程序的起点,main函数没有参数,也没有返回值,是程序的入口函数。

总结特征:

  1. 没有参数

  2. 没有返回值

  3. 会自动执行一次

  4. 在一个go文件内,main函数外的是全局作用域。

Go中读取用户的值可以用的 os 包 中的os.Args来获取

例子:

package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    number1, _ := strconv.Atoi(os.Args[1])
    number2, _ := strconv.Atoi(os.Args[2])
    fmt.Println("Sum:", number1+number2)
}

在终端运行

Go test.go 1 2 
// 打印出
Sum: 3

自定义函数

参数

go中前面的参数如果没有写类型,会找后面离自己最近的类型。

func sum(num1, num2 int, str1 string) string {}
// num1是int类型

可变参数

func computeSum(arge, ...int) int {}
computeSum(1,2,3,4,5)
// 在调试工具栈堆中可以看到arge => {[]int } len:5 , cap: 5。 也就是arge是一个切片来自

单返回值

package main

import (
	"fmt"
)

func main() {
	result := sum(1, 2)
	fmt.Println("Sum:", result)
}

//有两种写法
一种是
func sum(num1 int, num2 int) int {
	result := num1 + num2
	return result
}
//一种是为返回值设置变量,然后函数内部直接return,省略其返回值,也可以写上。
func sum(num1 int, num2 int) (result int) {
	result = num1 + num2
    // return result //result可写也可不写。
	return 
    
}

多返回值

package main

import (
	"fmt"
)

func main() {
	result, mul := sum(1, 2)
	fmt.Println("Sum:", result, mul)
}

func sum(num1 int, num2 int) (int, int) {
	result := num1 + num2
	mul := num1 * num2
	return result, mul
}

返回值类型指定返回变量名

package main

import (
	"fmt"
)

func main() {
	result, mul := sum(1, 2)
	fmt.Println("Sum:", result, mul)
}

func sum(num1 int, num2 int) (result int, mul int) {
	result = num1 + num2
	mul = num1 * num2
	return
}

更改函数参数值(指针)

Go 是“按值传递”编程语言。其实就是比JavaScript多了一种用指针修改变量值的方法。

package main

import "fmt"

func main() {
    firstName := "John"
    updateName(firstName)
    fmt.Println(firstName) // John
}

func updateName(name string) {
    name = "David"
}
//与JavaScript一样,不会被修改。

在 Go 中,有两个运算符可用于处理指针:

  • & 运算符,在变量前面加上&表示指向该变量的内存地址。
    • 运算符,在变量前面加上*表示取消引用指针,指向变量,而不是地址,与&反过来。
package main

import "fmt"

func main() {
    firstName := "John"
    updateName(&firstName)
    fmt.Println(firstName) // David
}

func updateName(name *string) {
    fmt.Println("内存地址:", name) //内存地址:0xc000026070
    *name = "David"    //*name指向name变量
}

go中的指针可以直接访问变量值,是go做了相应的处理,其他的静态语言是不可以的。

...语法

...在形参时写在变量前面,实参则写在变量后面

闭包:

与Javascript一致。

特性

  1. 内部函数可以访问到外部环境的变量
  2. 内部函数可以访问到外部函数的参数。
  3. 内部函数可以操作外部环境的变量和函数参数
  4. 闭包函数可以传入参数进行运算
  5. 闭包使外部函数变量或者参数成为内部函数的私有化变量

好处:

  1. 延长局部变量的生命周期
  2. 形成类似于面向对象的变量私有化特性
  3. 使外部作用域可访问到内部作用域变量

逃逸分析

Go编译器的优化技术,确定一个内部变量是否要在堆内存上分配内存空间的一种分析技术。

内部作用域访问的外部变量会被分配堆内存空间,没有访问到的不会分配。

package main

import "fmt"

// 未逃逸
func test() {
	a := 1
	test1 := func() {
		fmt.Println(a) // 这里因为执行完test也被销毁,并不存在,也就不存在逃逸。
	}

	test1()
}

// 逃逸
func test() {
	a := 1
	test1 := func() {
		// fmt.Println(a)  // 假如我把这条注释掉,那么a就不会产生逃逸,也就不会为a分配堆内存空间
	}

	return test1
}
func main() {
	test()
}

// 当内部作用域访问外部作用域的变量,这个变量就是持久化变量。

内置方法

main包

通常情况下,默认包是 main 包。 如果程序是 main 包的一部分,Go 会生成二进制文件,也就是生成可执行文件。 运行该文件时,它将调用 main() 函数。如果写的不是main包的一部分,那么不会生成可执行文件,而是生成包存档文件(.a后缀)

关于包命名的约定成俗

引入包路径的最后一部分作为包名称

公共与私有

Go与其他语言不同, 没有提供 public 或 private 关键字,而是使用名称的首字母大小来区别是公有还是私有。

  • 公有:可以在包外被调用
  • 私有:只能在包内被调用
//私有变量
var count = 1

//私有方法, 私有方法无法在包外被访问。
func sum(number1, number2 int) int {
    return number1 + number2
}

//公有变量
var Count = 1

// 公有方法
func Sum(number1, number2 int) int {
    return number1 + number2
}

包管理

go model模式

god.mod文件

go会自动管理god.mod,类似前端package.js。

包后面有注释indirect的是引入的包引用了其他包的包

god.sum则对应前端package-lock.js文件。

GoPATH模式

import的前提:

1、需要将代码新建到gopath/src之下,才能import

2、 Go111MODULE需要设置为off。(go env -w Go111MODULE=off)

import的顺序

先找gopath/src,找不到就回去找goroot/src目录之下找。

vender模式

这个模式解决GoPATH的版本管理问题

控制流

if/else

  1. 在Go中,if的条件不需要用()包裹起来,并且条件语句中的变量只能在if/else内部使用,在外部使用会报错。
  2. 判断条件必须是布尔值。

tips:Go中不使用括号包裹起来的好处就是如果复杂的条件判断,要用括号包裹起来,外层也有括号的话,就显得很混乱,且没有必要性。

package main

import "fmt"

func somenumber() int {
	return 1
}
func main() {
	if num := somenumber(); num < 0 {
		fmt.Println("小于0", num)
	} else if num < 10 {
		fmt.Println("大于0小于10", num)
	} else {
		fmt.Println("大于等于10", num)
	}

	// fmt.Println("num", num)  // undefined: num

}

switch

与JS不同点

  1. 与JS不同,在进入case后就会退出switch,不需要在case使用break来停止。
  2. 在case内部遇到break后,break后面的代码不会执行,会结束switch。
  3. Go中有fallthrough关键字,遇到fallthrough会进入下一个case,不会对case进行校验. (fallthroughz这个单词有贯穿意思)
  4. 一个case可以对应多个表达式,与JS不同,JS只能一个,而Go中可以有多个,使用逗号分隔
  5. 条件同样不需要括号
  6. switch可以不带条件,条件写在case、
package main

import (
	"fmt"
)

func main() {
	i := 1

	switch i {
	case 0, 1, 7: // 当i等于0或1会进入这个
		fmt.Print("进入第一个case")
	case 2:
		fmt.Print("第二个case")
	case 3:
		fmt.Print("第三个case")
	}
	fmt.Println("ok")
}
package main

import (
	"fmt"
)

func main() {
	i := 2

	switch i {
	case 0, 1:
		fmt.Print("进入第一个case")
	case 2:
		fmt.Print("第二个case")
		fallthrough   //遇到fallthrough进入下一个case的逻辑,不会校验i是否等于3
	case 3:
		fmt.Print("第三个case")
	}
	fmt.Println("ok")
}
// 执行结果为
// 第二个case第三个caseok
package main

import (
	"fmt"
)

func main() {
	switch {
	case true:
		fmt.Println("true")
	case 1 < 2:
		fmt.Println("false")
	default:
		fmt.Println("default")
	}
}

for循环

大体上与其他语言并无二致

break结束循环, Continue跳过循环的本次迭代。

package main

import "fmt"

func main() {
	// sum := 0
	// for i := 1; i <= 100; i++ {
	// 	sum += i
	// }
	// fmt.Println("输出1到100的和", sum)
	i := 0
	for { //可以不写,效果与while(true)一致,Go没有while是因为for完全可以代替while
		if i >= 10 {
			break
		}
		i++
	}
	fmt.Println("i的值", i)

}

for range

作用: 遍历有序集合:字符串、数组、切片、map、channel

package main

import "fmt"

func main() {
	str := "Hello World-测试"
	strRune := []rune(str)

	for index, element := range str {
		fmt.Printf("index: %d, element: %c\n", index, element) // 对于字符串,第二个值为该元素的Unicode码值(rune)。
	}

	for index, _ := range strRune {
		fmt.Printf("index: %d, element: %c\n", index, strRune[index])
	}

}

goto

作用:跳到标签名这块代码

格式:

goto 标签名

标签名:

执行语句

package main

import "fmt"

func main() {
	str := "Hello World-测试"
	// strRune := []rune(str)
	for index, element := range str {
		fmt.Println(index, element)
		goto respect
	}
respect:
	fmt.Println(str)
}
/*
 * 0 72
 * Hello World-测试
 */

错误捕获机制

  • 抛出异常:panic()
  • 捕获异常:recover(),类似于JavaScript的recover
  • 最终逻辑: defer,类似于JavaScript的finally
  • error机制:Go语言中,不认为所有的错误都需要捕获的方式去获取错误信息,每个可能抛出错误的方法都应该返回error。

defer 函数

用于推迟函数的执行时机,多个defer是倒序处理,先运行最后一个,最后运行第一个

package main

import "fmt"

func main() {
	defer fmt.Println("defer1")
	defer fmt.Println("defer2")
	defer fmt.Println("defer3")
	fmt.Println("test1")
	fmt.Println("test2")
	fmt.Println("test3")
}

// test1
// test2
// test3
// defer3
// defer2
// defer1

读取文件

package main

import (
	"bufio"
	"fmt"
	"os"
)

func ReadFile() {
	defer catchPanic()
	file, err := os.Open("test.txt")
	defer func(file *os.File) {
		if err := file.Close(); err != nil {
			panic("关闭文件失败")
		}
		fmt.Println("结束Close")
	}(file)

	if err != nil {
		panic("文件打开失败")
	}

	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		line := scanner.Text()
		fmt.Println("文本文件每一行", line)
	}
}

func catchPanic() {  
	if err := recover(); err != nil {
		fmt.Println("catch pamoc:", err)
	}
}
func main() {
	ReadFile()
}

疑问: catchPanic函数是最后执行的,为什么报错后能够继续执行这个函数来捕获错误。

泛型

泛型出现的原因

package main

import "fmt"

func main() {
	fmt.Println(plusInt(1, 2))
	fmt.Println(plusFloat(1.1, 1.2))
	fmt.Println(plusString("a", "b"))
}

func plusInt(a, b int) int {
	return a + b
}

func plusFloat(a, b float64) float64 {
	return a + b
}

func plusString(a, b string) string {
	return a + b
}

// 3
// 2.3
// ab

上面这里有个问题,这三个函数只有参数类型不一样,而无法整合成一个函数。

泛型就是解决这个问题的。

作用:

泛型是一个因为类型不确定而需要占位的标识符,在函数调用的时候,通过参数类型推断来确定实际泛型对应的具体类型。

定义泛型的方法

在Go语言中,泛型是需要在函数名后面进行定义才能使用。

  1. 在函数名后面跟[]
  2. 在中括号里写入泛型标识[T]
  3. 在Go语言中,泛型定义必须有类型约束(即泛型的可选值)
  4. 泛型标识原理上是可以写任何字符或单词

一般情况下

T代表Type

E代表Element

K代表Key

V代表Value

  1. 在函数调用的时候,泛型可以添入具体的类型来限制参数的类型,不写则Go进行类型推断,在调用的函数名后面写[Type]来限制。这里类似实参,而定义的时候类似形参
  2. 可以有多个泛型标识符,使用逗号分隔
  3. 泛型有个多个类型可以用interface提取出来。参考描述泛型的类型约束
package main

import "fmt"

func main() {
	fmt.Println(plus(1, 2))
	fmt.Println(plus(1.1, 1.2))
	fmt.Println(plus("a", "b"))
    fmt.Println(plus[int](1, 2))
}

func plus[T int | string | float64](a, b T) T {
	return a + b
}

// 3
// 2.3
// ab

any

代表任何类型都可以接收,是空interface的别名

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
package main

import "fmt"

func main() {
	myInfo := map[string]interface{}{
		"name": "zx",
		"age":  24,
	}
	myInfo1 := map[string]interface{}{
		"name": "wzx",
		"sex":  "男",
	}
	myAllInfo := make([]map[string]interface{}, 2)
	myAllInfo = append(myAllInfo, myInfo, myInfo1)
	forEachPrintSlice(myAllInfo)
}

func forEachPrintSlice[E any](s []E) {
	for _, e := range s {
		fmt.Println(e)
	}
}
// map[]
// map[]
// map[age:24 name:zx]
// map[name:wzx sex:男]

comparable

表示那些可以进行比较值,即能使用==、!=

package main

import "fmt"

func main() {
	fmt.Println(equals(1, 2))
	fmt.Println(equals("a", "a"))
	fmt.Println(equals('a', 'a'))
}

func equals[T comparable](a, b T) bool {
	return a == b
}

// map[]
// map[]
// map[age:24 name:zx]
// map[name:wzx sex:男]

自定义类型结合泛型使用

package main

import "fmt"

func main() {
	var sumSlice SumSlice[int] = []int{1, 2, 3, 4}
	res := sumSlice.Sum()
	fmt.Println(res) // 10
}

type SumSlice[T int | float64] []T

func (cs SumSlice[T]) Sum() T { // 这里的T同样类似形参
	var res T
	for _, e := range cs {
		res += e
	}
	return res
}

类型断言

在1.18之前,是没有泛型的,只能用类型断言来处理类型。类型断言只能对interface类型进行使用。

格式:

value, ok := i.(T) // T是断言的类型。ok是布尔值,返回是否断言成功。

package main

import "fmt"

func plus(a, b interface{}) interface{} {
	switch a.(type) {
	case int:
		_a, _ := a.(int)
		_b, _ := b.(int)
		return _a + _b
	case float64:
		_a, _ := a.(float64)
		_b, _ := b.(float64)
		return _a + _b
	case string:
		_a, _ := a.(string)
		_b, _ := b.(string)
		return _a + _b
	default:
		panic("类型不支持")
	}
}

func main() {
	res1 := plus(1, 2)
	res2 := plus(1.1, 2.2)
	res3 := plus("a", "b")
	res4 := plus(true, false)

	fmt.Println(res1)
	fmt.Println(res2)
	fmt.Println(res3)
	fmt.Println(res4)
    // panic: 类型不支持
}

结构体

  • 基于struct的自定义类型
  • Go语言中并没有面向对象的思想设计,class Object 对象性质的设计是没有的。
  • 结构体是一种类似于对象的结构的数据类型 (类似于C的结构体),可以实现面向对象的一部分特征
  • 结构体描述的是一组数据的结构,是一种Go语言层面上的自定义类型
  • 可以认为是多个字段结构
  • 结构体中如果包含不能相等性判断的类型,那么结构体与结构体就无法进行相等性判断

作用:封装多种类型的数据

声明一个结构体

//基于struct声明
type StructName struct {}

使用场景就是统一好数据中的字段,例如:

package main

import "fmt"

func main() {
	todo1 := map[string]interface{}{
		"id":        1,
		"content":   "todo1",
		"completed": false,
	}
	todo2 := map[string]interface{}{
		"id":        2,
		"content":   "todo2",
		"completed": false,
	}
	todo3 := map[string]interface{}{
		"id":        3,
		"content":   "todo3",
		"completed": false,
	}
	todoList := []map[string]interface{}{todo1, todo2, todo3}
	fmt.Println(todoList)
}

使用结构体统一字段

package main

import "fmt"

func main() {
	type Todo struct {
		// 字段名      字段类型
		id        int
		content   string
		completed bool
	}

	todo1 := Todo{
		1,
		"todo1",
		false,
	}

	todo2 := Todo{
		1,
		"todo1",
		false,
	}
	// 推荐这种写法
	/**
		1、字段名清晰,易读
		2、可以不完整的进行赋值
	**/
	todo3 := Todo{
		id:        3,
		content:   "todo3",
		completed: false,
	}
	// 修改结构体内的值
	todo3.content = "修改的todo3"
	todo3.completed = false
	todoList := []Todo{todo1, todo2, todo3}
	fmt.Println(todoList)
}

另一种初始化方式

package main

import "fmt"

func main() {
	type Todo struct {
		// 字段名      字段类型
		id        int
		content   string
		completed bool
	}
	todoList := []Todo{
		{
			id:        1,
			content:   "todo3",
			completed: false,
		},
		{
			id:        2,
			completed: false,
		},
		{
			id:        3,
			content:   "todo3",
			completed: false,
		},
		{},
		{completed: false},
	}
	fmt.Println(todoList)
}

使用new初始化

和上面两种的区别是new出来的是指针类型。

疑问: 因为类型不一样,就不能参与数据集合方法,那这个东西有什么用

package main

import "fmt"

func main() {
	type Todo struct {
		// 字段名      字段类型
		id        int
		content   string
		completed bool
	}
	todo := new(Todo)
	todo.id = 1
	todo.content = "Todo"
	todo.completed = true
	fmt.Println(todo)      // &{1 Todo true}
	fmt.Printf("%T", todo) // *main.Todo
}

策略模式

将值内的id,或者说代表着这组数据的值取出来做key,在后端经常使用这种模式,因为这就不需要去遍历数据组。

package main

import "fmt"

func main() {
	type Student struct {
		// 字段名      字段类型
		name   string
		age    int
		course string
	}
	students := map[int]Student{
		1: {
			name:   "张三",
			age:    18,
			course: "Java WEB",
		},
		2: {
			name:   "张三",
			age:    18,
			course: "Golang",
		},
		3: {
			name:   "张三",
			age:    18,
			course: "Rust",
		},
	}

	fmt.Println(students)

	for k, v := range students {
		fmt.Println(k, v)
	}
}

// map[1:{张三 18 Java WEB} 2:{张三 18 Golang} 3:{张三 18 Rust}]
// 1 {张三 18 Java WEB}
// 2 {张三 18 Golang}
// 3 {张三 18 Rust}

存储原理

内存对齐

首先分析存储原理首先需要先了解内存对齐,内存对齐也叫字节对齐。

内存对齐是一种空间换时间的办法,用于减少cpu的访问次数

(1) 机器的字长

也叫cpu位宽,是cpu里的寄存器宽度,是指cpu一次最多能处理二进制的位数

(2). 地址总线

cpu是通过地址总线来传送地址的,地址总线的位数决定CPU能直接寻址的内存空间大小,并且只能cpu传向外部存储器或I/O端口,是单向的。

(3). 数据总线

是数据是通过数据总线进行传输的,是双向的,数据总线的宽度是指一次可以传输的数据位数,

参考资料:www.bilibili.com/read/cv1932…

go官网解释的内存对齐

例子:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	type Test struct {
		a int8
		b int32
		c bool
	}

	// type Test struct { // 把b、c的顺序换下占用的字节就变了, 12 变成了 8
	// 	a int8
	// 	c bool
	// 	b int32
	// }

	test := Test{
		a: 1,
		b: 2,
		c: true,
	}
	fmt.Println(unsafe.Sizeof(test))  // 12  || 调换顺序后变成8
	fmt.Println(unsafe.Alignof(test)) // 4  获取对齐倍数
}

分析

  1. 首先第一个变量a,int8占1个字节,这个结构体里的字段最大是占4个字节,所以对齐倍数是4,所以struct会为变量分配4个连续空间,将a放进连续空间内,还是剩下3个
  2. 接下来处理b,b是int32,占4个字节,4个字符剩余的空间不够存放,无法进行内存对齐,所以重新又开辟了新的连续空间.存放在这个新的
  3. 接下是处理c,bool占1个字节,上一个已经存满了,所以重新又开辟了新的连续空间,存放进这个新的连续空间,剩下3个

第1个和第3个剩余的连续空间叫padding,就是浪费掉的空间。

如果现在将b、c调换一下位置

  1. 变量a还是第一步,
  2. 接下是处理c,bool占1个字节,进行内存对齐,存放进为a开辟的那个连续空间
  3. 接下来是b,b占4个字节,上一个不够放,在新开一个连续空间,存放进去

嵌套结构体

package main

import "fmt"

func main() {
	type Address struct {
		province string
		city     string
		district string
		street   string
		zipCode  string
	}

	type Logistics struct {
		orderId     string
		productName string
		province    string
		address     Address
	}

	logisticsInfo := Logistics{
		orderId:     "123456",
		productName: "iphone12",
		province:    "广东省xxx",
		address: Address{
			province: "广东省",
			city:     "广州市",
			district: "天河区",
			street:   "天河路",
			zipCode:  "510630",
		},
	}
	logisticsInfo.address.district = "黄埔区"
	fmt.Println(logisticsInfo) // {123456 iphone12 广东省xxx {广东省 广州市 黄埔区 天河路 510630}}
}

上面操作address内的字段,每次都得先address,而在go中,可以通过省略定义嵌套结构体中的结构体字段名,来省略address的访问

package main

import "fmt"

func main() {
	type Address struct {
		province string
		city     string
		district string
		street   string
		zipCode  string
	}

	type Logistics struct {
		orderId     string
		productName string
		province    string
		Address     /* 省略字段这种写法可以将Address里的属性平铺到Logistics中,
                 * 最后的结构体的结构与上面写法的结构还是一样的。
                 */
	}

	logisticsInfo := Logistics{
		"123456",
		"iphone12",
		"广东省xxx",
		Address{
			province: "广东省",
			city:     "广州市",
			district: "天河区",
			street:   "天河路",
			zipCode:  "510630",
		},
	}

	logisticsInfo.district = "黄埔区" // 不需要先访问address,可以直接访问下面的字段名
	fmt.Println(logisticsInfo) // {123456 iphone12 广东省xxx {广东省 广州市 黄埔区 天河路 510630}}
}

定义结构体上的方法

参考自定义类型-类型方法更改函数参数值

package main

import "fmt"

func main() {
	todo := Todo{
		id:        1,
		content:   "test",
		completed: false,
	}

	todo.setId(2)
	todo.setContent("test2")
	todo.setCompleted(true)
	fmt.Println(todo) // {2 test2 true}
}

type Todo struct {
	id        int
	content   string
	completed bool
}

func (todo *Todo) setId(id int) int {
	fmt.Println(todo) //&{1 test false}
	todo.id = id
	return id
}

func (todo *Todo) setContent(content string) string {
	todo.content = content
	return content
}

func (todo *Todo) setCompleted(completed bool) bool {
	todo.completed = completed
	return completed
}

范型结构体

package main

import "fmt"

func main() {
	test := Test[int]{
		a: 1,
		b: 2,
	}
	fmt.Println(test) // {1 2}
}

type Test[T int | float64] struct {
	a T
	b T
}

slice增删改查封装

package main

import "fmt"

type SliceData[T any] struct {
	data []T
	size int
}

func (sd *SliceData[T]) Remove(index int) (element T) {
	element = sd.data[index]
	sd.data = append(sd.data[:index], sd.data[index+1:]...)
	sd.size -= 1
	return element
}

func (sd *SliceData[T]) Size() (size int) {
	return sd.size
}

func (sd *SliceData[T]) Push(elements ...T) (index int) {
	sd.data = append(sd.data, elements...)
	sd.size += len(elements)
	index = len(sd.data) - 1
	return index
}

func (sd *SliceData[T]) Set(index int, element T) (oldElement T, newElement T) {
	oldElement = sd.data[index]
	newElement = element
	sd.data[index] = newElement
	return oldElement, newElement
}

func (sd *SliceData[T]) GetSlice() []T {
	return sd.data
}

func (sd *SliceData[T]) Get(index int) T {
	return sd.data[index]
}

func (sd *SliceData[T]) ForEach(cd func(
	element T,
	index int,
	data []T,
)) {
	for index, element := range sd.data {
		cd(element, index, sd.data)
	}
}

func (sd *SliceData[T]) Map(cd func(
	element T,
	index int,
	data []T,
) T) []T {
	newSlice := make([]T, len(sd.data))

	for index, element := range sd.data {
		newEl := cd(element, index, sd.data)
		newSlice[index] = newEl
	}

	return newSlice
}
func main() {
	dataSlice := SliceData[int]{
		data: []int{1, 2, 3, 4, 5},
		size: 5,
	}
	fmt.Println(dataSlice) // {[1 2 3 4 5] 5}

	idx := dataSlice.Push(6, 7, 8)
	fmt.Println(idx, dataSlice) // 7 {[1 2 3 4 5 6 7 8] 8}

	elem := dataSlice.Remove(3)

	fmt.Println(elem, dataSlice) // 4 {[1 2 3 5 6 7 8] 7}
	el := dataSlice.Get(2)
	slice := dataSlice.GetSlice()
	fmt.Println(el, dataSlice, slice) // 3 {[1 2 3 5 6 7 8] 7} [1 2 3 5 6 7 8]
	dataSlice.ForEach(func(element, index int, data []int) {
		fmt.Println(element, index, data)
	})

	newDataSlice := dataSlice.Map(func(element, index int, data []int) int {
		return element * 2
	})
	fmt.Println(newDataSlice, dataSlice.GetSlice()) // [2 4 6 10 12 14 16] [1 2 3 5 6 7 8]
	dataSlice.Set(1, 200)
	fmt.Println(dataSlice.GetSlice()) // [1 200 3 5 6 7 8]
}

interface

interface是接口,用于规范方法的实现的标准。

就是设计一个类型,里面要需要有什么方法,我们写的时候就按这个定义好的interface去实现方法。

对比起来就是interface就相当与原型,我们根据interface实现的变量就相当我们开发出来的程序。

作用:1、接口可以用来描述泛型的类型约束

2、空字符描述泛型的类型约束。即interface{},也是any

package main

type MyInt int

type Plus interface {
	int | string | float32 | float64
}
// 加上~s是包含衍生类型(相关的自定义类型)的含义,例如~int就还包括MyInt
// type Plus interface {
// 	~int | string | ~float32 | float64
// }

// func plus[T int | string | float32 | float64](a, b T) T {
// 	return a + b
// }

func plus[T Plus](a, b T) T {
	return a + b
}

func main() {}

鸭子类型

动态语言中的鸭子类型 (typescript举例)

就是抽象出相同行为的类型,并不是指具体的某个类型。

如typescript可以使用abstract class ClassName(){}来实现,也可以用inteface来实现

  • 抽象类是不能实例化的类型、只能被继承
  • 抽象类内部是抽象方法和属性的定义
  • 当然抽象类也可以定义具体的方法和属性
  • 所有抽象成员都不需要进行实现或者初始化
  • 实现或者初始化交给子类去完成
  • 抽象成员不可以被private修饰

作用:规定子类需要实现什么方法

/* 
 *  一般来说,如果抽象类没有定义方法,使用inteface来做鸭子类型会更好点,
 *  比如去掉getName的话就用inteface。因为鸭子类型的含义就是以方法进行抽象区分类型。
 */
abstract class Duck {
  abstract name: string;
  abstract walk(): void;
  abstract shout(): void;
  getName(){
    console.log("我是:" + this.name);
  }
}
// 子类必须实现抽象类的方法,就是加了abstract关键字的那些
class Bird extends Duck {
  name: string;
  constructor(name: string) {
    super();
    this.name = name;
  }
  walk(): void {
    console.log("Bird walking");
  }
  shout(): void {
    console.log("Bird shouting");
  }
}

class Person extends Duck {
  name: string;
  constructor(name: string) {
    super();
    this.name = name;
  }
  walk(): void {
    console.log("Person walking");
  }
  shout(): void {
    console.log("Person shouting");
  }

  getAge(){ // 对外暴露的方法一定是要定义在抽象类里面,所以这个方法外部是调用不到的。

  }
}

// 一样是Duck是为了限制子类必须实现抽象类的方法,就是加了abstract关键字的那些
const bird: Duck = new Bird("小鸟");
const person: Duck = new Person("人类");

bird.walk();
person.walk();
person.getAge() // 类型“Duck”上不存在属性“getAge”。ts(2339)

上面的例子有点不清晰,把例子换成地图就清晰了。


// 地图类型都有定位和搜索功能
// 下面基于CommonMap抽象类来实现地图的定位和搜索功能(行为)的百度地图和高德地图。

abstract class CommonMap {
  abstract locate(): void;
  abstract search(): void;
  getType(){
    if(this instanceof baiduMap){
      return  '百度地图'
    }else{
      return '高得地图'
    }
  }
}

class baiduMap extends CommonMap {
  locate(): void {
    console.log("baidu定位成功");
  }
  search(): void {
    console.log("baidu搜索结果");
  }
}

class aMap extends CommonMap {
  locate(): void {
    console.log("amap获取定位");
  }
  search(): void {
    console.log("amap搜索结果");
  }

  getAge(){ // 对外暴露的方法一定是要定义在抽象类里面,所以这个方法外部是调用不到的。

  }
}

// 一样是CommonMap是为了限制子类必须实现抽象类的方法,就是加了abstract关键字的那些
const bird: CommonMap = new baiduMap();
const gaode: CommonMap = new aMap();

bird.locate();
gaode.search();

console.log(bird.getType());
console.log(gaode.getType());

静态语言中的鸭子类型 (JAVA)

Go中的鸭子类型

package main

import "fmt"

type CommonMap interface { // 鸭子
	locate()
	search()
}

type BaiduMap struct {
	len int
}

func (bd *BaiduMap) locate() {
	fmt.Println("baidu定位成功")
}

func (bd *BaiduMap) search() {
	fmt.Println("baidu搜索结果")
}

type AMap struct {
	len int
}

func (bd *AMap) locate() {
	fmt.Println("AMap定位成功")
}

func (bd *AMap) search() {
	fmt.Println("Amap搜索结果")
}

func doMap(plat CommonMap) {
	plat.locate()
	plat.search()
}

func main() {
	var baiduMaop CommonMap = new(BaiduMap)
	var aMap CommonMap = new(AMap)
	doMap(baiduMaop)
	doMap(aMap)
}

// baidu定位成功
// baidu搜索结果
// AMap定位成功
// Amap搜索结果

go中的继承或者合并:嵌套方式

type A interface{
	plus()
}

type B interface{
	A
	minus()
}

单元测试

go test

  1. go test会运行包目录中,所有以_test.go结尾的源码文件都会被go test执行。
  2. _test.go测试文件不会被go build命令打包进可执行文件
  3. test文件里三类测试
    • Test开头的是单元测试,参数必须是t *testing.T,无返回值
    • Benchmark开头的是基准测试,参数必须是 b *testing.B,无返回值。
    • Example开头的是模糊测试,参数必须是 f *testing.F,无返回值

在单元测试中,直接写测试函数就行,不需要在main函数里写。

例子:

包结构

calculator

------sum_test.go

------sum.go

package calculator

import "testing"

func TestAdd(t *testing.T) {
	res := add(1, 2)
	if res != 3 {
		t.Errorf("expect 3, actual %d", res)
	}
}
package calculator

func add(a, b int) int {
	return a + b
}

跳过单元测试

func TestAdd(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping test in short mode.") // 在go test -short下,只会执行到这里,就不会再往下执行了
	}
    
	res := add(1, 2)
	if res != 3 {
		t.Errorf("expect 3, actual %d", res)
	}
}

基于表驱动测试

表驱动法(table-driven Approach ),是数据驱动编程的一种。

package calculator

import (
	"testing"
)

func TestAdd(t *testing.T) {
	dataset := []struct {
		a   int
		b   int
		out int
	}{
		{a: 1, b: 2, out: 3},
		{a: 2, b: 2, out: 4},
		{a: 3, b: 3, out: 6},
	}
	for _, d := range dataset {
		res := add(d.a, d.b)
		if res != d.out {
			t.Errorf("expect %d, actual %d", d.out, res)
		}
	}
}

性能测试

package calculator

import (
	"bytes"
	"fmt"
	"strings"
	"testing"
)

const (
	testString   = "Hello, World!"
	testIter     = 10000
	expectedSize = len(testString) * testIter
)

func BenchmarkStringConcatenation(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		result := ""
		for j := 0; j < testIter; j++ {
			result += testString
		}
		if len(result) != expectedSize {
			// Unexpected result size
			b.Errorf("结果大小不符合预期: %d", len(result))
		}
	}
	b.StopTimer()
}

func BenchmarkStringBuilder(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		var builder strings.Builder
		for j := 0; j < testIter; j++ {
			builder.WriteString(testString)
		}
		result := builder.String()
		if len(result) != expectedSize {
			b.Errorf("结果大小不符合预期: %d", len(result))
		}
	}
	b.StopTimer()

}

func BenchmarkByteBuffer(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var buffer bytes.Buffer
		for j := 0; j < testIter; j++ {
			buffer.WriteString(testString)
		}
		result := buffer.String()
		if len(result) != expectedSize {
			b.Errorf("结果大小不符合预期: %d", len(result))
		}
	}
	b.StopTimer()

}

func BenchmarkStringsJoin(b *testing.B) {
	for i := 0; i < b.N; i++ {
		result := ""
		for j := 0; j < testIter; j++ {
			result = strings.Join([]string{result, testString}, "")
		}
		if len(result) != expectedSize {
			b.Errorf("结果大小不符合预期: %d", len(result))
		}
	}
}

运行: go test -bench . 或者 go test -bench=".*"

其中,BenchmarkStringConcatenation、BenchmarkStringBuilder 和 BenchmarkByteBuffer、BenchmarkStringsJoin 是测试代码中定义的三个测试函数。

"ns/op" 是指每个操作(operation)所花费的时间,单位为纳秒(nanoseconds)。

12267 102427 ns/op 的输出是该函数执行的统计信息。具体地,12267 表示测试循环总共执行了多少次;102427 ns/op 表示每个测试循环执行的平均时间。

最后一行输出 PASS 表示测试通过

Tip:

1、变量声明后,变量必须得被使用,否则会报错

2、Go官方不推荐在一个文件夹下有多个main, 一个文件不允许有多个main,

3、在文件引入main包情况下,如果没有定义main,该文件不执行。

4、运行GO文件时main方法会自动被调用,main就是Go程序的入口方法

5、一个包的内部调用。不需要包名引导。go是以包为单位,不是文件。例如,同一个包下有logic.go、words.go,logic.go去调用words.go的方法直接调用即可,不需要加上包名点上方法

6、一个项目的开始和前端一样,先初始包管理,go mod init 项目名生成go.mod文件