包(package)是多个go源码的集合,一个包可以简单理解为一个存放多个 .go 文件的文件夹。该文件夹下面的所有 go文件都要在代码的第一行添加 package xxx,来声明该文件归属的包。(非注释的第一行)
注意事项:
在编写go程序时,一般都需要导入对应的包才能实现指定功能。导入方式:
import (
"fmt" // 导入go语言的标准库,go有很多标准库,如:time os
"study/lib1" // 这是使用的是 go module 的方式引入本地包(本地的module名为study,当前文件与lib1包在同一文件夹下的情况)
"lib1" // 这里导入的是 GOPATH/src中的包。其实就是将当前文件夹的lib1文件夹复制过去(随着GOPATH被go module取代,这种方式见到的会越来越少)
"./lib1" // 采用相对路径导包 该方法在现有使用 go module 的情况下将不能使用,此处仅作示例
abc "study/lib1" // 采用别名导包。(导入的包名有重复,或者包名太长时使用)
_ "study/lib1" // 采用匿名方式导包。因为导包之后必须要使用,否则编译不通过。而当只是需要该包的init函数,又不需要其他函数时,就用到了匿名导包
. "study/lib1" // 采用.的方式导包。直接导入到了当前文件内,调用不需要带包名。但可能会产生函数名冲突等问题,应该少用
"fyne.io/fyne/v2/app" // 这就是普通的导入的线上的包
// 如果一个包被多个包同时导入,在编译运行时实际只会被导入一次。
)
go的包管理器。
常用命令:
使用第三方包的操作顺序:
go mod init 项目名称(如果是已经存在的项目,则省略此步骤)
配置第三方包——在代码中 import ,并且使用包内函数
go mod tidy 下载依赖
运行项目——正常。
//
开头的表示单行注释/*
与 */
包起来的内容为多行注释(块注释)区别:
fmt.Print("A", "B", "C")
输出的内容中间没有空格。fmt.Println()
输出的内容中间有空格。fmt.Sprintf()
可以将各类数据类型连接输出成字符串。如:fmt.Sprintf("ab=%v", 10)
当使用 fmt.Printf()
、fmt.Sprintf()
输出时:
init()函数执行顺序:
go编译器会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
运行时,最后被导入的包会最先初始化,并调用其 init() 函数。(简单实测:会按照层级进行init函数的调用,相同层级的init函数的调用顺序可能不与代码内的上下文顺序一致)
func main(){}
同文件内也可以定义 init()
,同样的init()
会优先执行。
Go是强类型语言,不会做任何隐式的类型转换。—— int 类型 和 uint 类型也不能对比是否相等。
类型 | 范围 | 占用字节数 |
---|---|---|
int8 | -2⁷ ~ 2⁷-1 | 1 |
int16 | -2¹⁵ ~ 2¹⁵-1 最多5位数字 | 2 |
int32 | -2³¹ ~ 2³¹-1 最多10位数字 | 4 |
int64 | -2⁶³ ~ 2⁶³-1 最多19位数字 | 8 |
uint8 | 1 | |
uint16 | 2 | |
uint32 | 4 | |
uint64 | 8 |
类型 | 范围 | 占用字节数 |
---|---|---|
float32 | 最大值 3.4e38 | 4 |
float64 | 最大值 1.8e308 | 8 |
可以使用 math.MaxFloat32 、 math.MaxFloat64 来输出对应浮点数的最大值来查看。
科学计数法,就是用普通的e记录(十进制的):var num1 float32 = 3.14e4 var num2 float64 = 2.46e-3
go 通过第三方包来解决计算小数时丢失精度的问题: "github.com/shopspring/decimal"
float 转换为 int 会直接丢弃小数位。num1 := int(2.34)
当使用fmt.Printf() 输出时:
unsafe.Sizeof()
查询string类型变量占用的字节数量总是16字节,这与底层使用的结构体指针有关。应该使用 len()
来查看string类型变量占用的空间。string的底层结构体实现:
// file: /reflect/value.go 1.18之后如此,1.17之前类似
type StringHeader struct {
Data uintptr
Len int
}
[]byte
类型切片。64位机器下占8个字节。len()
函数获取这个值。注意,len存储实际的字节数,而非字符数。// 上面所说的字符串不能修改,指的是不能对单个字符修改,但是可以整体重新赋值。
// 整体的重新赋值,相当于在Go的底层新创建了一个[]byte{}类型的切片,将变量s中的指针指向了新的内存空间地址(也就是这里的Hello)。原有的hello内存空间会随着垃圾回收机制被回收掉。
s := "hello"
s = "Hello"
// 如果想要修改字符串的某一个字符,或者通过下标的方式来修改字符,应该将字符串定义成[]byte格式。
ss := []byte("abc")
ss[1] = '3'
fmt.Println(string(ss)) // a3c
转义符 | 含义 |
---|---|
\r | 回车符(返回行首)(Carriage Return) |
\n | 换行符(直接跳到下一行的同列位置)(Line Feed)(可是实测与\r效果一致) |
\t | 制表符 |
' " \ | 输出不同的符号 |
其他扩展知识:
在任何系统中,文件内容都有换行的概念。其中:
CR,即 Carriage Return,表示回车。
LF,即 Line Feed,表示换行。用于Linux系统
CRLF,即 Carriage Return & Line Feed,表示回车并换行。用于Windows系统。
常用方法:
方法 | 介绍 |
---|---|
len(str) | 求字符串的长度 |
+号 或 fmt.Sprintf() | 拼接字符串 |
strings.Split() | 分割 |
strings.Join() | 数组/切片 转 字符串 |
strings.Contains(A,B) | 判断A串是否包含B串 |
strings.HasPrefix() / strings.HasSuffix() | 前缀 / 后缀 判断 |
strings.Index() / strings.LastIndex() | 从前往后 / 从后往前 查找子串的位置。从0开始,找不到返回 -1 |
字符与字符串不一样,它只能是单个字符,并且用单引号定义。
直接输出时(%v),英文为 ascii 码,汉字为utf-8编码(Unicode编码的十进制表示)
使用 %c 才能原样输出内容。%T 类型输出为 int32 。
Go语言的字符有两种表示方式:
var b1 = 'a' // 定义一个字符,可以定义成数字、字母、符号、汉字。使用单引号定义
fmt.Printf("%v %c %T\n", b1, b1, b1) // 结果输出为: 97 a int32
var b2 = '好'
fmt.Printf("%v %c %T\n", b2, b2, b2) // 输出结果为: 22909 好 int32
byte 与 rune 在打印汉字时的区别:
s := "你好 go"
for i := 0; i < len(s); i++ { // 这种方式默认的字符类型为uint8(byte),输出中文时会有问题
fmt.Printf("%v(%c)", s[i], s[i])
}
for _, v := range s { // 使用 range 时,默认为 rune 类型,可以正常输出中文
fmt.Printf("%v(%c)", v, v)
}
aa := 12.6 // 需要先定义浮点数变量,不能直接使用int(12.6)
a := int(aa) // 浮点数 强制转化为 整型int,会抛弃小数位,不做四舍五入
var b int8 = 12
c := int16(b) // 将 int8 转化为 int16
var d float32 = 12.3
e := float64(d) // 将 float32 转化为 float64
首先讲一个字符串拼接:两个字符串可以通过 +
来实现拼接,其底层会被解析为一个字符串相加的表达式-AddStringExpr。
package main
import (
"fmt"
"strconv"
)
func main() {
d := 1
f := 2.356
t := true
c := 'a'
str1 := fmt.Sprintf("%d", d)
fmt.Printf("int: %v %T\n", str1, str1)
str2 := fmt.Sprintf("%f", f)
fmt.Printf("float: %v %T\n", str2, str2)
str3 := fmt.Sprintf("%t", t)
fmt.Printf("bool: %v %T\n", str3, str3)
str4 := fmt.Sprintf("%c", c)
fmt.Printf("byte: %v %T\n", str4, str4)
fmt.Println()
str11 := strconv.FormatInt(int64(d), 10)
fmt.Printf("int: %v %T\n", str11, str11)
str21 := strconv.FormatFloat(float64(f), 'f', 2, 64)
fmt.Printf("float: %v %T\n", str21, str21)
str31 := strconv.FormatBool(t) // 没什么意义的转换
fmt.Printf("bool: %v %T\n", str31, str31)
str41 := strconv.FormatUint(uint64(c), 10) // 没什么意义的转换
fmt.Printf("byte: %v %T\n", str41, str41) // 输出的内容是字符的ascii编码
/*
以上输出为:
int: 1 string
float: 2.356000 string
bool: true string
byte: a string
int: 1 string
float: 2.36 string
bool: true string
byte: 97 string
*/
}
func main() {
s := []byte{77, 89, 90}
fmt.Println(*(*string)(unsafe.Pointer(&s)))
}
字符串转换为整型、浮点型
字符串也可以转换为字符型,即使用 for-range 使用字符的rune类型进行转换,可以转中文。
字符串转换为布尔型没有什么意义:
<nil>
。a, _ := strconv.ParseInt("123", 10, 64)
b, _ := strconv.ParseFloat("123.456", 64)
fmt.Printf("a=%d a.type=%T b=%f b.type=%T", a, a, b, b)
// 输出为: a=123 a.type=int64 b=123.456000 b.type=float64
!!!如下几种方式的使用是会报错的,无法像Js一样只取出相应类型的值
strconv.ParseInt("123.456", 10, 64)
strconv.ParseInt("123你好", 10, 64)
strconv.ParseFloat("123你好", 64)
使用的可能比较少
修改字符串,即修改字符串中的某一个字符或汉字。
因为字符在表示时,有 uint8(byte) 和 rune 两种类型。所以在修改时,根据是否包含中文,也要区分为两种类型。
byte
类型,修改之后再强制转换成字符串。rune
类型,修改之后再强制转换成字符串。byte
类型变量的存储类型为 []uint8
。rune
类型变量的存储类型为[]int32
。它们存储的都是Ascii码。
具体代码如下:
package main
import (
"fmt"
)
func main() {
ss := "big" // 不含中文的情况
byteSs := []byte(ss) // 强制转换为 byte 类型
byteSs[0] = 'p' // 需要修改为单引号的字符,而不是双引号的字符串
fmt.Println(string(byteSs)) // 在输出时需要强制转换为字符串。该行输出:pig
s2 := "早上好12" // 包含中文的情况
runeS2 := []rune(s2) // 强制转换为 rune 类型
runeS2[0] = '晚'
fmt.Println(string(runeS2)) // 该行输出:晚上好12
}
数组:
切片:
切片是基于数组类型做的一个封装。
切片是一个引用类型(体现在赋值和传参),内部结构包含地址、长度、容量。
不支持两个切片直接比较,只能和 nil
比较。
可以基于数组定义一个切片,也可以基于切片定义一个切片。
在声明方式上,数组与切片的唯一区别就是数组需要指定长度,切片不用指定长度。
// 1. 固定长度的数组,数组元素会默认为存储类型的0值
var myArr1 [5]int
// 2. 只给一部分声明值,其余元素也是会默认为0
myArr2 := [6]int{1, 2, 3}
// 3. 可以使用3个点来自动推测数组长度,这样声明后的数组长度依旧不能改变
myArr3 := [...]int{33, 22, 2346, 39}
// 4. 可以指定元素的下标来进行初次赋值,这样会根据最大下标来决定数组长度,未赋值元素默认为0值
myArr4 := [...]int{1: 8, 4: 4, 5: 6}
// 多维数组
// 5. 声明一个普通的二维数组
arr1 := [3][2]int{{23, 45}, {1, 2}, {9, 0}}
// 6. 二维数组也可以像一维数组一样,在外层采用3个点来让编译器自动推测长度
arr2 := [...][2]int{{23, 45}, {1, 2}, {9, 0}}
package main
import (
"fmt"
)
func main() {
// 1. 声明一个切片,并且初始化
s1 := []int{11, 22, 33}
fmt.Println(s1)
// 2. 基于数组创建的一个切片。
arr1 := [7]int{3, 55, 66, 77, 432, 9, 10} // 先声明一个数组
// abc := arr1 // 这样复制出来的就是一个数组,与arr1完全一致
slice1 := arr1[:] // 这样复制出来的是一个切片,复制数组的第一个元素至最后一个元素
slice2 := arr1[:3] // 复制出一个切片,左闭右开:[3 55 66],len=3,cap=7
slice3 := arr1[2:4] // 左闭右开:[6 77],len=2,cap=5
slice4 := arr1[3:] // 左闭右开:[77 432 9, 10],len=4,cap=4
// 基于切片复制出一个切片的操作是一致的。
fmt.Println(slice1)
fmt.Printf("slice2: len=%v cap=%v val=%v\n", len(slice2), cap(slice2), slice2)
fmt.Printf("slice3: len=%v cap=%v val=%v\n", len(slice3), cap(slice3), slice3)
fmt.Printf("slice4: len=%v cap=%v val=%v\n", len(slice4), cap(slice4), slice4)
// 3. 使用make()函数的声明
ss1 := make([]int, 5) // 声明一个类型为[]int,长度为5,容量为5 的切片,默认值为[0 0 0 0 0]
ss2 := make([]int, 5, 6) // 声明一个类型为[]int,长度为5,容量为6 的切片,默认值为[0 0 0 0 0]
fmt.Println(ss1, ss2)
}
// 1. 数组的声明:
[cap]type
// [容量]类型 例如: [5]int
// 数组的 len() 和 cap() 一致。
// 2. 切片的声明:
[]type
// 使用make进行初始化切片,3个参数的情况
make([]type, length, capacity)
// 使用make进行初始化切片,2个参数的情况,初始化一个长度和容量均为length的切片
make([]type, length)
切片不能采用下标法直接扩容,需要使用 append() 来扩。
可以使用 append() 可以:一次扩容1个元素、一次扩容多个元素、合并两个切片。
切片在扩容时,其容量是由编译器自动管理的,扩容策略会根据切片的长度不同以及类型不同有所区分。
可以通过查看 $GOROOT/src/runtime/slice.go 源码,其中扩容方法为:func growslice();
package main
import (
"fmt"
)
func main() {
s1 := []int{} // 声明一个空的切片
fmt.Printf("%v %#v %T %d %d\n", s1, s1, s1, len(s1), cap(s1))
s1 = append(s1, 1) // 一次扩容1个元素
fmt.Printf("%v %T %d %d\n", s1, s1, len(s1), cap(s1))
s1 = append(s1, 2, 3, 4) // 一次扩容多个元素
fmt.Printf("%v %T %d %d\n", s1, s1, len(s1), cap(s1))
n3 := []int{11, 12, 13}
s1 = append(s1, n3...) // 合并两个切片,后面切片需要加 ...
fmt.Printf("%v %T %d %d\n", s1, s1, len(s1), cap(s1))
}
package main
import (
"fmt"
)
func main() {
s1 := []int{11, 12, 13}
c1 := s1 // 直接赋值为引用传递
fmt.Println(s1, c1) // [11 12 13] [11 12 13]
c1[0] = 20 // c1的改动将会影响s1
fmt.Println(s1, c1) // [20 12 13] [20 12 13]
c2 := make([]int, 5) // 这里make一个切片,对长度及容量并没有特别限制
le := copy(c2, s1) // copy为值传递。总是从第一个元素开始copy。总是返回两者长度的最小值:le = min(len(c2), len(s1))
fmt.Println(le) // 3
fmt.Println(c2) // [20 12 13 0 0]
c2[0] = 100 // c2的改动不影响s1
fmt.Println(s1) // [20 12 13]
fmt.Println(c2) // [100 12 13 0 0]
}
func main() {
s1 := []int{11, 12, 13, 14, 15}
s1 = append(s1[:2], s1[3:]...) // 删除索引为2的元素
fmt.Println(s1)
}
package main
import (
"fmt"
"sort"
)
func main() {
intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0}
float8List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9, 31.4, 27.81828, 3.14}
stringList := []string{"a", "c", "b", "z", "x", "w", "y", "d", "f", "i"}
// 升序排
sort.Ints(intList)
sort.Float64s(float8List)
sort.Strings(stringList)
fmt.Println(intList)
fmt.Println(float8List)
fmt.Println(stringList)
// 降序排
sort.Sort(sort.Reverse(sort.IntSlice(intList)))
sort.Sort(sort.Reverse(sort.Float64Slice(float8List)))
sort.Sort(sort.Reverse(sort.StringSlice(stringList)))
fmt.Println(intList)
fmt.Println(float8List)
fmt.Println(stringList)
}
package main
import (
"fmt"
"math"
"slices"
)
func main() {
// int类型排序(升序)
sIntSlice := []int{11, 34, 7, 3, 67}
slices.Sort(sIntSlice)
fmt.Println(sIntSlice)
// Float类型排序
// slices.Sort()官方注释:When sorting floating-point numbers, NaNs are ordered before other values.
// 即:在给浮点数排序时,如果切片中有值为NaN,则会排在前面
sFloatSlice := []float64{1, 43.8, 4, 0.8, math.NaN(), 2, math.NaN()}
slices.Sort(sFloatSlice)
fmt.Println(sFloatSlice)
// string类型排序
sStringSlice := []string{"LiMing", "Bob", "Li4", "Ai"}
slices.Sort(sStringSlice)
fmt.Println(sStringSlice)
// Reverse() 可以将数组元素倒置
ss := []int{10, 3, 45, 8}
slices.Reverse(ss)
fmt.Println(ss)
}
数组 | 切片 | |
---|---|---|
直接初始化 | √ | √ |
make | × | √ |
访问元素 | arr[i] | arr[i] |
len | 长度 | 已有元素的个数 |
cap | 长度 | 容量 |
可扩容 | × | √ |
append | × | √ |
func main() {
var mp = make(map[string]int) // 声明一个map,可以不指定长度、容量
fmt.Println(mp)
fmt.Printf("%T\n", mp)
mp["one"] = 1 // 可以直接使用下标法来进行扩容
mp["two"] = 2
fmt.Println(mp)
fmt.Println(len(mp)) // 需要使用 len() 来获取map的长度
mp2 := map[string]string{ // 也可以在声明时赋值
"name": "张三",
"age": "18",
}
fmt.Println(mp2)
}
func main() {
mp2 := map[string]string{
"name": "张三",
"age": "18",
}
for k, v := range mp2 {
fmt.Printf("k=%v v=%v\n", k, v)
}
}
func main() {
mp2 := map[string]string{
"name": "张三",
"age": "18",
}
fmt.Println(mp2)
vv, ok := mp2["name2"] // 判断 name2 是否存在
fmt.Println(vv, ok)
}
func main() {
mp2 := map[string]string{
"name": "张三",
"age": "18",
}
fmt.Println(mp2) // map[age:18 name:张三]
delete(mp2, "name")
fmt.Println(mp2) // map[age:18]
delete(mp2, "name123")
fmt.Println(mp2) // map[age:18]
}
// 普通的写法
const (
n1 = iota // 0
n2 // 1
n3 // 2
)
// 中间插队的写法
const (
a1 = iota // 0
a2 = 100 // 100
a3 = iota // 2
a4 // 3
)
// 使用匿名函数的写法
const (
b1 = iota // 0
_
b3 // 2
b4 // 3
)
const (
a1, a2 = iota + 1, iota + 1
a3, a4 = iota + 2, iota + 3
)
// 实际: a1=1 a2=1 a3=3 a4=4
// 可以理解为同一行的iota相等,往下一行 iota+1
func main() {
var a = new(int) // 这里主动声明一个指针类型,并分配空间
fmt.Printf("%T %v %v\n", a, a, *a) // *int 0xc00000a0b8 0 分配空间之后,*a就有了当前数据类型的默认值
*a = 10 // 可以直接给 *a 赋值
fmt.Println(*a) // 10
var b *int // 这里只声明一个指针类型,但是没有分配空间
fmt.Printf("%T %v\n", b, b) // *int <nil> 由于没有分配空间,所以值为 <nil>
// *b = 20 分配空间之前直接赋值会报错
// fmt.Println(*b) 甚至连打印都报错
b = new(int) // 手动分配空间
fmt.Printf("%T %v %v\n", b, b, *b) // 分配空间之后,一切都正常了
}
func main() {
var mp1 map[string]string
fmt.Printf("%v %v\n", mp1, len(mp1)) // map[] 0
// mp1["name"] = "li4" // 不能给未分配空间的map直接赋值,会报错
mp1 = make(map[string]string) // 必须要make一下,才能赋值
mp1["name"] = "张三"
fmt.Println(mp1) // map[name:张三]
}
a := 10
b := 20
// 方法1
t := a
a = b
b = t
// 方法2
a = a + b
b = a - b
a = a - b
// 方法3
a, b = b, a