golang基础知识
Golang
基本语法
导出
导出和访问控制是通过命名来进行实现的,如果想要对外暴露一个函数或者一个变量,只需要将其名称首字母大写即可
package example
import "fmt"
// 可外部调用
var A int = 1
// 不可外部调用
var b int = 2
// 可外部调用
func ExternalExample(){
fmt.Println("Here is an external example")
}
// 不可外部调用
func internalExample(){
fmt.Println("Here is an internal example")
}
字母和数字
letter(字母) = unicode_letter | "_" .
decimal_digit(十进制数) = "0" … "9" .
binary_digit(二进制数) = "0" | "1" .
octal_digit(八进制数) = "0" … "7" .
hex_digit(十六进制数) = "0" … "9" | "A" … "F" | "a" … "f" .
下划线(_)被视为小写字母
标识符
标识符为变量和类型等程序实体命名。
标识符是由一个或多个字母和数字组成的序列。标识符中的第一个字符必须是字母。
identifier = letter { letter | unicode_digit } .
注释
- 单行注释: //
- 多行注释: /**/
关键字
- 要注意命名冲突
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
运算符
- 数字越大优先级越高
- go语言中没有选择将
~
作为取反运算符,而是复用了^
符号,当两个数字使用^
时,例如a^b
,它就是异或运算符,只对一个数字使用时,例如^a
,那么它就是取反运算符 - go支持自增与自减,a++ 或 a--,不返回值
优先级 运算符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
- 位清除操作符,用于将左操作数中指定位置上的位清零(置为0),然后将结果赋值给左操作数
package main
import "fmt"
func main() {
a := 7 // 二进制表示为 0111
a &^= 2 // 将 a 中与 2 对应位置上为 1 的位清零
fmt.Println(a) // 输出结果为 5,二进制表示为 0101
}
字面量
- 整数字面量
整数字面量是表示整数常量的一系列数字
允许使用下划线_
来进行数字划分,但是仅允许在前缀符号之后和数字之间使用
int_lit = decimal_lit | binary_lit | octal_lit | hex_lit .
decimal_lit = "0" | ( "1" … "9" ) [ [ "_" ] decimal_digits ] .
binary_lit = "0" ( "b" | "B" ) [ "_" ] binary_digits .
octal_lit = "0" [ "o" | "O" ] [ "_" ] octal_digits .
hex_lit = "0" ( "x" | "X" ) [ "_" ] hex_digits .
decimal_digits = decimal_digit { [ "_" ] decimal_digit } .
binary_digits = binary_digit { [ "_" ] binary_digit } .
octal_digits = octal_digit { [ "_" ] octal_digit } .
hex_digits = hex_digit { [ "_" ] hex_digit } .
24 // 24
024 // 24
2_4 // 24
0_2_4 // 24
10_000 // 10k
100_000 // 100k
0O24 // 20
0b00 // 0
0x00 // 0
0x0_0 // 0
- 浮点数字面量
浮点文字是浮点常数的十进制或十六进制表示形式
通过不同的前缀可以表达不同进制的浮点数
float_lit = decimal_float_lit | hex_float_lit .
decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
decimal_digits decimal_exponent |
"." decimal_digits [ decimal_exponent ] .
decimal_exponent = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .
hex_float_lit = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
hex_mantissa = [ "_" ] hex_digits "." [ hex_digits ] |
[ "_" ] hex_digits |
"." hex_digits .
hex_exponent = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .
0.
72.40
072.40 // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5. // == 15.0
0.15e+0_2 // == 15.0
0x1p-2 // == 0.25
0x2.p10 // == 2048.0
0x1.Fp+0 // == 1.9375
0X.8p-0 // == 0.5
0X_1FFFP-16 // == 0.1249847412109375
0x15e-2 // == 0x15e - 2 (integer subtraction)
- 虚数字面量
虚数表示复数常数的虚数部分
它由 整数 或 浮点文字 组成后跟小写字母
虚字面的值是相应的 整数 或 浮点数 乘以虚数单位 i
imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .
0i
0123i // == 123i for backward-compatibility
0o123i // == 0o123 * 1i == 83i
0xabci // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i // == 0x1p-2 * 1i == 0.25i
- 字符串字面量
字符串文字表示通过连接字符序列获得的字符串常量。
有两种形式: 原始字符串文字和解释字符串文字。
string_lit = raw_string_lit | interpreted_string_lit .
raw_string_lit = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc` // 与 "abc" 相同
`\n
\n` // 与 "\\n\n\\n" 相同
"\n"
"\"" // 与 `"` 相同
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800" // 非法的: 仅一半代理
"\U00110000" // 非法的: 无效 Unicode字符
符文字符
转义字符: 反斜杠后,某些单字符转义表示特殊值
\a U+0007 响铃符号(建议调高音量)
\b U+0008 回退符号
\f U+000C 换页符号
\n U+000A 换行符号
\r U+000D 回车符号
\t U+0009 横向制表符号
\v U+000B 纵向制表符号
\\ U+005C 反斜杠转义
\' U+0027 单引号转义 (该转义仅在字符内有效)
\" U+0022 双引号转义 (该转义仅在字符串内有效)
符文文字中反斜杠后面的无法识别的字符是非法的
rune_lit = "'" ( unicode_value | byte_value ) "'" .
unicode_value = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value = `\` "x" hex_digit hex_digit .
little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit .
escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'\'' // 包含单引号字符的符文字符
'aa' // 非法的: 太多字符
'\k' // 非法的: 在反斜杠之后不能识别 k
'\xa' // 非法的: 不合法的十六进制数,位数不够
'\0' // 非法的: 不合法的八进制数,位数不够
'\400' // 非法的: 不合法的八进制数,大于255
'\uDFFF' // 非法的: 仅一半代理
'\U00110000' // 非法的: 无效 Unicode字符
数据类型
布尔类型
只有真
和假
true
为真值,false
为假值
数值类型
- 整数
uint8 所有无符号8 位整数的集合 (0 to 255)
uint16 所有无符号16位整数的集合 (0 to 65535)
uint32 所有无符号32位整数的集合 (0 to 4294967295)
uint64 所有无符号64位整数的集合 (0 to 18446744073709551615)
int8 所有有符号8 位整数的集合 (-128 to 127)
int16 所有有符号16位整数的集合 (-32768 to 32767)
int32 所有有符号32位整数的集合 (-2147483648 to 2147483647)
int64 所有有符号64位整数的集合 (-9223372036854775808 to 9223372036854775807)
- 浮点数
float32 所有 IEEE-754 32位浮点数的集合
float64 所有 IEEE-754 64位浮点数的集合
- 复数
complex64 带有 float32实数和虚数部分的所有复数的集合
complex128 带有 float64实数和虚数部分的所有复数的集合
- 字符
byte 等价 uint8 可以表达ANSCII字符
rune 等价 int32 可以表达Unicode字符
string
byte 类型通常用于处理二进制数据或字节流,例如文件读写、网络通信
在字符串操作中,byte 类型经常被用作字符串的字符元素类型
派生类型
类型 | 例子 |
---|---|
数组 | [3]int ,长度为 3 的整数型数组 |
切片 | []float64 ,64位浮点数切片 |
映射表 | map[string]int ,键为字符串类型,值为整数型的映射表 |
结构体 | type Gopher struct{} ,名为Gopher 的结构体 |
指针 | *int ,int类型指针。 |
函数 | type f func() ,一个名为 f 的函数,没有参数,没有返回值 |
接口 | type Gopher interface{} ,名为Gopher 的接口 |
通道 | chan int ,整数型通道 |
零值
zero value
零值并不仅仅只是字面上的数字零,而是一个类型的空值或者说默认值更为准确
类型 | 零值 |
---|---|
数字类型 | 0 |
布尔类型 | false |
字符串类型 | "" |
数组 | 固定长度的对应类型的零值集合,例如声明类型,但为赋值 |
结构体 | 内部字段都是零值的结构体 |
切片,映射表,函数,接口,通道,指针 | nil |
nil
nil
仅仅只是一个变量。
Go中的nil
并不等同于其他语言的null
,nil
仅仅只是一些类型的零值,并且不属于任何类型,
所以nil == nil
这样的语句是无法通过编译的。
常量
常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:
-
其他常量标识符
-
常量表达式
-
结果是常量的类型转换
- 例如,const MyConstant = int32(42):将整数
42
显式转换为int32
类型,并将其赋值给常量MyConstant
- 例如,const MyConstant = int32(42):将整数
-
iota
常量只能是基本数据类型,不能是
- 除基本类型以外的其它类型,如结构体,接口,切片,数组等
- 函数的返回值
注意:常量的值无法被修改,如果尝试对其进行修改的话将会无法通过编译
初始化
-
常量的声明需要用到
const
关键字,常量在声明时就必须初始化一个值,并且常量的类型可以省略const name string = "Jupiter" // 字面量 const msg = "hello world" // 字面量 const num = 1 // 字面量 const numExpression = (1+2+3) / 2 % 100 + num // 常量表达式
-
声明时不可只声明类型,初始化即需要赋值
-
批量声明常量可以用
()
括起来以提升可读性,可以存在多个()
达到分组的效果const ( age = 18 Name = "Jupiter" ) const ( Size = 160 Len = 180 )
-
在同一个常量分组中,在已经赋值的常量后面的常量可以不用赋值,其值默认就是前一个的值
const ( A = 1 B // 1 C // 1 D // 1 E // 1 )
iota
是一个在常量声明中使用的预定义标识符,用于生成一系列相关常量的递增值
iota 默认是递增的,也就是+1
const (
Num = iota // 0
Num1 // 1
Num2 // 2
Num3 // 3
Num4 // 4
)
const (
Num = iota*2 // 0
Num1 // 2
Num2 // 4
Num3 // 6
Num4 // 8
)
const (
Num = iota << 2*3 + 1 // 1
Num1 // 13
Num2 // 25
Num3 = iota // 3
Num4 // 4
)
变量
声明
// 格式为var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则
var age int
var name string
var char byte
// 声明多个相同类型的变量时,可以只写一次类型
var A, B, C int
// 声明多个不同类型的变量时,可以使用()进行包裹,可以存在多个()
var (
name string
age int
sex string
)
var (
school string
class int
)
赋值
// 赋值会用到运算符=
var name string
name = "jupiter"
// 可以声明时即赋值
var name string = "jupiter"
// 也可以一行赋多值
var a,b int
a, b = 1, 2
// 方提供的语法糖:短变量初始化(:=),可以省略掉var关键字和后置类型,具体是什么类型交给编译器自行推断
// 后续赋值时,类型必须保持一致
// 短变量初始化不能使用nil,因为nil不属于任何类型,编译器无法推断其类型
name := "jupiter"
// 短变量声明可以批量初始化
name, age := "jupiter", 18
// 短变量声明方式无法对一个已存在的变量使用,但可以在赋值旧变量的同时声明一个新的变量
a := 1
a := 2 // 错误
a, b := 2, 3 // 可行
// 所有在函数中的变量都必须要被使用,函数外的包级变量则没有这个限制
交换
// 如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换
a, b := 1, 2
a, b = b, a
// 由于在函数内部存在未使用的变量会无法通过编译,但有些变量又确实用不到,这个时候就可以使用匿名变量_,使用_来表示该变量可以忽略
a, b, _ := 1, 2, 3 // 更多的是接收函数的返回值而不是举例的直接声明
比较
// 相同类型的变量才可进行比较
// 不同类型的变量需要经过强制类型转换才能比较
var a uint64
var b int64
fmt.Println(int64(a) == b)
// 比较最大值和最小值
// 不同的go版本,min和max所支持的数据类型不同,1.21版本后用泛型重写了min和max
min(1, 2, 1.2)
max(2, -1, 100)
//可比较类型有:
- 布尔
- 数字
- 字符串
- 指针
- 通道 (仅支持判断是否相等)
- 元素是可比较类型的数组(切片不可比较)
- 字段类型都是可比较类型的结构体(仅支持判断是否相等)
// 还可以通过导入标准库cmp来判断,不过仅支持有序类型的参数,在go中内置的有序类型只有数字和字符串
cmp.Compare(1, 2)
比较两个值的大小,并返回一个表示比较结果的整数
如果第一个值小于第二个值,返回负整数(通常为 -1)。
如果第一个值等于第二个值,返回零。
如果第一个值大于第二个值,返回正整数(通常为 1)。
cmp.Less(1, 2)
判断第一个值是否小于第二个值。
如果第一个值小于第二个值,则返回 true,否则返回 false。
代码块
//在函数内部,可以通过花括号建立一个代码块,代码块彼此之间的变量作用域是相互独立的
func main() {
a := 1
{
a := 2
fmt.Println(a) // 输出 2
}
{
a := 3
fmt.Println(a) // 输出 3
}
fmt.Println(a) // 输出 1
}
// 块与块之间的变量相互独立,不受干扰,无法访问,但是会受到父块中的影响
// 也就是全局变量和局部变量的区别
func main() {
a := 1
{
a := 2
fmt.Println(a) // 输出 2
}
{
fmt.Println(a) // 输出 1
}
fmt.Println(a) // 输出 1
}
输入输出
标准
在os
包下有三个外暴露的文件描述符,其类型都是*File
,分别是:
Stdin
- 标准输入Stdout
- 标准输出Stderr
- 标准错误
Go中的控制台输入输出都离不开它们。
输出
常用的方法有三种
// 第一种
os.Stdout.WriteString("Hello 世界!")
// 第二种
println("Hello 世界!")
// 第三种
fmt.Println("Hello 世界!")
提示:
fmt.Println
会用到反射,输出的内容更容易阅读,不过性能不好。
格式化
格式 | 说明 | 接收类型 |
---|---|---|
%% | 输出百分号% |
任意类型 |
%s | 输出string /[] byte 值 |
string,[] byte |
%q | 格式化字符串,输出的字符串两端有双引号"" |
string,[] byte |
%d | 输出十进制整型值 | 整型类型 |
%f | 输出浮点数 | 浮点类型 |
%e | 输出科学计数法形式 ,也可以用于复数 | 浮点类型 |
%E | 与%e 相同 |
浮点类型 |
%g | 根据实际情况判断输出%f 或者%e ,会去掉多余的0 |
浮点类型 |
%b | 输出整型的二进制表现形式 | 数字类型 |
%#b | 输出二进制完整的表现形式 | 数字类型 |
%o | 输出整型的八进制表示 | 整型 |
%#o | 输出整型的完整八进制表示 | 整型 |
%x | 输出整型的小写十六进制表示 | 数字类型 |
%#x | 输出整型的完整小写十六进制表示 | 数字类型 |
%X | 输出整型的大写十六进制表示 | 数字类型 |
%#X | 输出整型的完整大写十六进制表示 | 数字类型 |
%v | 输出值原本的形式,多用于数据结构的输出 | 任意类型 |
%+v | 输出结构体时将加上字段名 | 任意类型 |
%#v | 输出完整Go语法格式的值 | 任意类型 |
%t | 输出布尔值 | 布尔类型 |
%T | 输出值对应的Go语言类型值 | 任意类型 |
%c | 输出Unicode码对应的字符 | int32 |
%U | 输出字符对应的Unicode码 | rune,byte |
%p | 输出指针所指向的地址 | 指针类型 |
代码示例:
func main() {
fmt.Printf("%%%s\n", "hello world") // %hello world
fmt.Printf("%s\n", "hello world") // hello world
fmt.Printf("%q\n", "hello world") // "hello world "
fmt.Printf("%d\n", 2<<7-1) // << 位运算符,十进制2转换为二进制就是10,向左移7位,100000000,再减1,就是11111111,最后输出为255
fmt.Printf("%f\n", 1e2) // 1乘以10的平方,也就是100,输出为100.000000
fmt.Printf("%e\n", 1e2) // 1.000000e+02
fmt.Printf("%E\n", 1e2) // 1.000000e+02
fmt.Printf("%g\n", 1e2) // 100
fmt.Printf("%b\n", 2<<7-1) // 11111111
fmt.Printf("%#b\n", 2<<7-1) // 0b11111111
fmt.Printf("%o\n", 2<<7-1) // 377
fmt.Printf("%#o\n", 2<<7-1) // 0377
fmt.Printf("%x\n", 2<<7-1) // ff
fmt.Printf("%#x\n", 2<<7-1) // 0xff
fmt.Printf("%X\n", 2<<7-1) // FF
fmt.Printf("%#X\n", 2<<7-1) // 0XFF
type person struct {
name string
age int
address string
}
fmt.Printf("%v\n", person{"lihua", 22, "beijing"})
// {lihua 22 beijing}
fmt.Printf("%+v\n", person{"lihua", 22, "beijing"})
// {name:lihua age:22 address:beijing}
fmt.Printf("%#v\n", person{"lihua", 22, "beijing"})
// main.person{name:"lihua", age:22, address:"beijing"}
fmt.Printf("%t\n", true) // true
fmt.Printf("%T\n", person{}) // main.person
fmt.Printf("%c%c\n", 20050, 20051) // 乒乓
fmt.Printf("%U\n", '你') // U+4F60
fmt.Printf("%p\n", &person{}) // 0xc0000c2900
// 使用其它进制时,在%与格式化动词之间加上一个空格便可以达到分隔符的效果
str := "abcdefg"
fmt.Printf("%x\n", str) // 61626364656667
fmt.Printf("% x\n", str) // 61 62 63 64 65 66 67
// 在使用数字时,还可以自动补零
fmt.Printf("%09d", 1) // 000000001
fmt.Printf("%09b", 1<<3) // 000001000
}
输入
通常使用fmt
包下提供的三个函数Scan、Scanln、Scanf
// 扫描从os.Stdin读入的文本,根据空格分隔,换行也被当作空格
// func Scan(a ...any) (n int, err error)
func main() {
var s, s2 string
fmt.Scan(&s, &s2)
fmt.Println(s, s2)
}
// 与Scan类似,但是遇到换行停止扫描
// func Scanln(a ...any) (n int, err error)
func main() {
var s, s2 string
fmt.Scanln(&s, &s2)
fmt.Println(s, s2)
}
// 根据格式化的字符串扫描
// func Scanf(format string, a ...any) (n int, err error)
func main() {
var s, s2, s3 string
scanf, err := fmt.Scanf("%s %s \n %s", &s, &s2, &s3)
if err != nil {
fmt.Println(scanf, err)
}
fmt.Println(s)
fmt.Println(s2)
fmt.Println(s3)
}
缓冲
当对性能有要求时可以使用bufio
包进行读取
// 读
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
fmt.Println(scanner.Text())
}
// 写
func main() {
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("hello world!\n")
writer.Flush()
fmt.Println(writer.Buffered())
}
条件控制
if else
if expression {
}
// 或
if expression {
}else {
}
// expression必须是一个布尔表达式,即结果要么为真要么为假,必须是一个布尔值
// 可以是比较,也可以是一个只返回布尔值的函数
// 也可以在 expression 中声明变量
func main() {
if x := 1 + 1; x > 2 {
fmt.Println(x)
}
}
else if
else if
语句可以在if else
的基础上创建更多的判断分支
if expression1 {
}else if expression2 {
}else if expression3 {
}else {
}
// 举例
func main() {
score := 90
var ans string
if score >= 0 && score < 60 {
ans = "F"
} else if score < 70 {
ans = "D"
} else if score < 80 {
ans = "C"
} else if score < 90 {
ans = "B"
} else if score < 100 {
ans = "A"
} else if score == 100 {
ans = "S"
}else {
ans = "nil"
}
fmt.Println(ans)
}
switch
switch
语句也是一种多分支的判断语句
switch expr {
case case1:
statement1
case case2:
statement2
default:
default statement
}
// 还可以在表达式之前编写一些简单语句,例如声明新变量
func main() {
switch num := f(); { // 等价于 switch num := f(); true {
case num >= 0 && num <= 1:
num++
case num > 1:
num--
fallthrough
case num < 0:
num += num
}
}
func f() int {
return 1
}
// switch语句也可以没有入口处的表达式。
// 通过`fallthrough`关键字来继续执行相邻的下一个分支
func main() {
num := 2
switch {
case num >= 0 && num <= 1:
num++
case num > 1:
num--
fallthrough // 执行完该分支后,会继续执行下一个分支
case num < 0:
num += num
}
fmt.Println(num)
}
label
标签语句,给一个代码块打上标签,可以是goto
,break
,continue
的目标
func main() {
A:
a := 1
B:
b := 2
}
goto
goto
将控制权传递给在同一函数中对应标签的语句
在实际应用中goto
用的很少,跳来跳去的很降低代码可读性
存在性能消耗问题
unc main() {
a := 1
if a == 1 {
goto A
} else {
fmt.Println("b")
}
A:
fmt.Println("a")
}
循环控制
for
for init statement; expression; post statement {
execute statement
}
// 当只保留循环条件时,for 可以当成是while
for expression {
execute statement
}
// 输出 0~20 间的整数
func main() {
for i := 0; i <= 20; i++ {
fmt.Println(i)
}
}
// 双循环打印九九乘法
func main() {
for i := 1; i <= 9; i++ {
for j := 1; j <= 9; j++ {
if i <= j {
fmt.Printf("%d*%d = %2d ", i, j, i*j)
}
}
fmt.Println()
}
}
for range
for range
可以更加方便的遍历一些可迭代的数据结构,例如:数组,切片,字符串,映射表,通道
for index, value := range iterable {
}
// index为可迭代数据结构的索引
// value则是对应索引下的值
// 例如使用for range遍历一个字符串。
func main() {
sequence := "hello world"
for index, value := range sequence {
fmt.Println(index, string(value))
}
}
// 输出如下:
0 h
1 e
2 l
3 l
4 o
5
6 w
7 o
8 r
9 l
10 d
break
break
关键字会终止最内层的for
循环
func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i <= j {
break
}
fmt.Println(i, j)
}
}
}
结合标签一起使用可以达到终止外层循环的效果
func main() {
Out:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i <= j {
break Out
}
fmt.Println(i, j)
}
}
}
continue
continue
关键字会跳过最内层循环的本次迭代,直接进入下一次迭代
func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i > j {
continue
}
fmt.Println(i, j)
}
}
}
结合标签使用可以达到跳过外层循环的效果
func main() {
Out:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i > j {
continue Out
}
fmt.Println(i, j)
}
}
}
数组
如果事先就知道了要存放数据的长度,且后续使用中不会有扩容的需求,就可以考虑使用数组,
Go中的数组是值类型,而非引用,并不是指向头部元素的指针。
初始化
// 数组在声明是长度只能是一个常量,**不能是变量**
var a [5]int
var a [(1 + 2) * 3]int
const len = 5
var b [len]int
var c [len + 1]int
// 可以用元素初始化
nums := [5]int{1, 2, 3}
// 可以通过new函数获得一个指针,
nums := new([5]int)
使用
// 只要有数组名和下标,就可以访问数组中对应的元素
fmt.Println(nums[0])
// 可以修改数组元素
nums[0] = 1
// 可以通过内置函数len来访问数组元素的数量
len(nums)
// 内置函数cap来访问数组容量,数组的容量等于数组长度,容量对于切片才有意义
cap(nums)
切割
// 切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开
nums := [5]int{1, 2, 3, 4, 5}
nums[1:] // 子数组范围[1,5) -> [2 3 4 5]
nums[:5] // 子数组范围[0,5) -> [1 2 3 4 5]
nums[2:3] // 子数组范围[2,3) -> [3]
nums[1:3] // 子数组范围[1,3) -> [2 3]
切片
在Go中,数组和切片两者看起来长得几乎一模一样,但功能有着不小的区别,
数组是定长的数据结构,长度被指定后就不能被改变,
而切片是不定长的,切片在容量不够时会自行扩容。
切片的底层实现依旧是数组,是引用类型,可以简单理解为是指向底层数组的指针。
新 slice 预留的 buffer容量 大小是有一定规律的。
在golang1.18版本更新之前网上大多数的文章都是这样描述slice的扩容策略的:
当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
在1.18版本更新之后,slice的扩容策略变为了: 当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4 。
初始化
var nums []int // 值 默认值为nil,所以不会为其分配内存
nums := []int{1, 2, 3} // 值
nums := make([]int, 0, 0) // 值 推荐
nums := new([]int) // 指针
使用
切片的基本使用与数组完全一致,区别只是切片可以动态变化长度
切片可以通过append
函数实现许多操作
func append(slice []Type, elems ...Type) []Type
// `slice`是要添加元素的目标切片
// `elems`是待添加的元素
// 返回值是添加后的切片
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 可以看到长度与容量并不一致。
插入元素
切片元素的插入也是需要结合append
函数来使用
道理差不多就是灵活地拼接切片
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 从头部插入元素
nums = append([]int{-1, 0}, nums...) // [-1 0 1 2 3 4 5 6 7 8 9 10]
// 从中间下标i插入元素
nums = append(nums[:i+1], append([]int{99, 99}, nums[i+1:]...)...)
// i=3,[1 2 3 4 99 99 5 6 7 8 9 10]
// 从尾部插入元素
nums = append(nums, 99, 100) // [1 2 3 4 5 6 7 8 9 10 99 100]
删除元素
切片中间部分元素的删除也是需要结合append
函数来使用
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 从头部删除n个元素
nums = nums[n:] //n=3 [4 5 6 7 8 9 10]
// 从尾部删除n个元素
nums = nums[:len(nums)-n] //n=3 [1 2 3 4 5 6 7]
// 从中间指定下标i位置开始删除n个元素
nums = append(nums[:i], nums[i+n:]...) // i=2,n=3,[1 2 6 7 8 9 10]
// 删除所有元素
nums = nums[:0] // []
拷贝
切片在拷贝时需要确保目标切片有足够的长度
func main() {
dest := make([]int, 0) // 注意这里的 0
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(src, dest)
fmt.Println(copy(dest, src))
fmt.Println(src, dest)
}
>>>>>>
[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []
func main() {
dest := make([]int, 9) // 注意这里的 9
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(src, dest)
fmt.Println(copy(dest, src))
fmt.Println(src, dest)
}
>>>>>>
[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0]
0
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9]
遍历
切片的遍历与数组完全一致,for
循环
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
for index, val := range slice {
fmt.Println(index, val)
}
}
多维切片
var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}
>>>>
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[]
[]
[]
[]
[]
// 二维数组在初始化时已经固定好一维和二维的长度
// 切片的长度是不固定的,所以切片中的每一个切片可能是不同长度的,必须要单独初始化
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}
>>>>
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
拓展表达式
只有切片才能使用拓展表达式
拓展表达式,主要是为了解决切片共享底层数组的读写问题,也就是利用容器不足,产生新底层数组
左闭右开
// low <= high <= max <= cap
// 使用拓展表达式切割的切片容量为 max-low
slice[low:high:max]
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:5]
fmt.Println(s2)
fmt.Println(cap(s1), cap(s2))
>>>>
[4 5]
9 6
// 复现共享底层数组的问题
// 添加新元素,由于容量为6.所以没有扩容,直接修改底层数组
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)
>>>
[4 1]
[1 2 3 4 1 6 7 8 9] // 将 5 改成了 1
// 使用拓展表达式,使容量不足
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4:4] // cap = 4 - 3 = 1
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)
>>>>
[4 1]
[1 2 3 4 5 6 7 8 9]
clear
在go1.21新增了clear
内置函数,clear会将切片内所有的值置为零值
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}
// 清空切片
s = s[:0:0]
字符串
字面量
字符串有两种字面量表达方式,分为普通字符串和原生字符串
-
普通字符串
由
""
双引号表示,支持转义,不支持多行书写。"这是一个普通字符串\n" "abcdefghijlmn\nopqrst\t\\uvwxyz" >>>> 这是一个普通字符串 abcdefghijlmn opqrst \uvwxyz
-
原生字符串
由反引号表示,不支持转义,支持多行书写,原生字符串里面所有的字符都会原封不动的输出,包括换行和缩进。
`这是一个原生字符串,换行 tab缩进,\t制表符但是无效,换行 "这是一个普通字符串" 结束 ` >>>> 这是一个原生字符串,换行 tab缩进,\t制表符但是无效,换行 "这是一个普通字符串" 结束
访问
字符串本质是字节数组,所以字符串的访问形式跟数组切片完全一致
字符串元素无法直接修改
func main() {
str := "this is a string"
fmt.Println(str[0])
}
>>> 116 // 输出是字节而不是字符
// 切割
func main() {
str := "this is a string"
fmt.Println(string(str[0:4]))
}
>>>> this
// 覆盖
func main() {
str := "this is a string"
str = "that is a string"
fmt.Println(str)
}
转换
字符串可以转换为字节切片,而字节切片或字节数组也可以转换为字符串
func main() {
str := "this is a string"
// 显式类型转换为字节切片
bytes := []byte(str)
fmt.Println(bytes) // [116 104 105 115 32 105 115 32 97 32 115 116 114 105 110 103]
// 显式类型转换为字符串
fmt.Println(string(bytes)) // this is a string
}
字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的
func main() {
str := "this is a string"
fmt.Println(&str) // 0xc00004a6f0
bytes := []byte(str)
// 修改字节切片
bytes = append(bytes, 96, 97, 98, 99)
// 赋值给原字符串
str = string(bytes)
fmt.Println(str) // this is a string`abc
fmt.Println(&str) // 0xc00004a6f0
}
// 总结下来就是,先转换成字节切片,对字节切片进行调整修改,最后再覆盖原先的字符串
// 字符串→字节切片→字符串(覆盖)
注意:两种类型之间的转换都需要进行数据拷贝,其性能损耗会随着长度的增加而增长。
长度
字符串的长度,其实并不是字面量的长度,而是字节数组的长度,只是大多数时候都是ANSCII
字符,刚好能用一个字节表示,所以恰好与字面量长度相等
用内置函数len
来获取字符串长度
func main() {
str := "this is a string" // 看起来长度是16
str2 := "这是一个字符串" // 看起来长度是7
fmt.Println(len(str), len(str2)) // 16 21
}
拷贝
类似数组切片的拷贝方式,字符串拷贝其实是字节切片拷贝
可以使用内置函数copy
或者strings.clone
func main() {
var dst1, dst2, src string
src = "this is a string"
desBytes := make([]byte, len(src))
// copy
copy(desBytes, src)
dst1 = string(desBytes)
fmt.Println(src, dst1)
// clone
dst2 = strings.Clone(src)
fmt.Println(src, dst2)
}
拼接
字符串的拼接使用+
操作符,也可以转换为字节切片再进行添加元素
如果对应性能有更高要求,可以使用strings.Builder
func main() {
// +
str := "this is a string"
str1 = str + " that is a int"
fmt.Println(str1)
// 转换切片
bytes := []byte(str)
bytes = append(bytes, "that is a int"...)
str2 = string(bytes)
fmt.Println(str2)
// strings.Builder
builder := strings.Builder{}
builder.WriteString("this is a string ")
builder.WriteString("that is a int")
fmt.Println(builder.String())
}
遍历
属于ASCII字符的,一个字符占 1 个字节
一个中文字符会占用 3 个字节
按照字节来遍历(遍历数组中的每个元素)会把中文字符拆开,会出现乱码
Go字符串是明确支持utf8的,应对这种情况就需要用到rune
类型
在使用for range
进行遍历时,其默认的遍历单位类型就是一个rune
func main() {
str := "hello 世界!"
for _, r := range str {
fmt.Printf("%d,%x,%s\n", r, r, string(r))
}
}
>>>>
104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!
也可以转换成[]rune
切片
func main() {
str := "hello 世界!"
runes := []rune(str)
for i := 0; i < len(runes); i++ {
fmt.Println(string(runes[i]))
}
}
还可以使用uft8
包下的工具
func main() {
str := "hello 世界!"
for i, w := 0, 0; i < len(str); i += w {
r, width := utf8.DecodeRuneInString(str[i:])
fmt.Println(string(r))
w = width
}
}
映射表
在Go中,map
的实现是基于哈希桶(一种哈希表),是无序的。
map的键类型必须是可比较的,比如string
,int
是可比较的,而[]int
是不可比较的,也就无法作为map的键。
初始化
-
字面量
mp1 := map[int]string{ 0: "a", 1: "a", 2: "a", 3: "a", 4: "a", } mp2 := map[string]int{ "a": 0, "b": 22, "c": 33, }
-
make
在初始化map时应当尽量分配一个合理的容量,以减少扩容次数
mp := make(map[string]int, 8)
访问
map对于不存的键其返回值是对应类型的零值
在访问map时,有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
if val, exist := mp["f"]; exist {
fmt.Println(val)
} else {
fmt.Println("key不存在")
}
}
依旧可以用len
查看map长度
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(len(mp))
}
存值
存值时使用已存在的键会覆盖原有的值
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
if _, exist := mp["b"]; exist {
mp["b"] = 3
}
fmt.Println(mp)
}
应当尽量避免使用NaN
作为map的键,无法覆盖,还会导致存在多个
任何数字都不等于NaN
,NaN
也不等于自身,这也造成了每次哈希值都不相同
删除
删除一个键值对
需要用到内置函数delete
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp)
delete(mp, "a")
fmt.Println(mp)
}
// 无法删除 NaN 键值对
遍历
通过for range
可以遍历map
map是无序存储,所以结果会与定义时的键值对顺序不同
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
for key, val := range mp {
fmt.Println(key, val)
}
}
清空
使用内置的clear函数(go 1.21后)
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
clear(m)
fmt.Println(m)
}
set
Set是一种无序的,不包含重复元素的集合
可以使用map来替代set,也就有键但不设置值的map
func main() {
set := make(map[int]struct{}, 10)
for i := 0; i < 10; i++ {
set[rand.Intn(100)] = struct{}{}
}
fmt.Println(set)
}
注意
map 没有引入互斥锁,并不适合高并发场景的写操作,会触发fatal error
指针
创建
取地址符&
解引用符*
对一个变量进行取地址,会返回对应类型的指针
指针存储的是变量num
的地址
解引用符则有两个用途
- 访问指针所指向的元素,也就是解引用
- 声明一个指针
func main() {
num := 2
p := &num
rawNum := *p
var p2 *int
p2 = new(int)
fmt.Println(p) // 0xc0000149c0
fmt.Println(rawNum) // 2
fmt.Println(p2) // 0xc0000149c8
}
禁止指针运算
在Go中是不支持指针运算的,也就是说指针无法偏移
标准库unsafe
提供了许多用于低级编程的操作,其中就包括指针运算
new 和 make
func new(Type) *Type
- 返回值是类型指针
- 接收参数是类型
- 专用于给指针分配内存空间
func make(t Type, size ...IntegerType) Type
- 返回值是值,不是指针
- 接收的第一个参数是类型,不定长参数根据传入类型的不同而不同
- 专用于给切片,映射表,通道分配内存。
结构体
结构体可以存储一组不同类型的数据,是一种复合类型
。
声明
结构体本身以及其内部的字段都遵守大小写命名的暴露方式。
type Rectangle struct {
height, width, area int
color string
}
注意:在声明结构体字段时,字段名与方法名不应该重复
创建
// 不存在构造方法,大多数情况下采用如下的方式来创建
student := Student{
Name: "jupiter",
Age: 18,
Sex: man
}
// 也可以省略字段名称,省略的同时必须初始化所有字段
student := Student{
"jupiter",
18,
man
}
也可以编写一个函数来专门初始化结构体,这类函数通常有另一个名称:工厂方法
组合
结构体之间的关系是通过组合来表示的,可以显式组合,也可以匿名组合,后者使用起来更类似于继承
-
显示组合
type Person struct { name string age int } type Student struct { p Person // Person 是上面声明的结构体类型 school string } student := Student{ p: Person{name: "jupiter", age: 18}, school: "High school", } fmt.Println(student.p.name)
-
匿名组合
type Student struct { Person // 注意此处的区别 school string } student := Student{ Person: Person{name: "jack",age: 18}, school: "lili school", } fmt.Println(student.name)
指针
对于结构体指针而言,不需要解引用就可以直接访问结构体的内容
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)
// 在编译的时候会转换为(*p).name ,(*p).age
// 其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖。
标签
标签是一种键值对的形式,使用空格进行分隔。
结构体标签的容错性很低,如果没能按照正确的格式书写结构体,那么将会导致无法正常读取,但是在编译时却不会有任何的报错。
type Student struct {
Name string `json:"name"`
Age int `yaml:"age"`
Sex string `toml:"sex"`
}
内存对齐
Go结构体字段的内存分布遵循内存对齐的规则,这么做可以减少CPU访问内存的次数,属于空间换时间的一种手段。
结构体的内存占用长度至少是最大字段的整数倍,不足的则补齐。
空结构体
空结构体没有字段,不占用内存空间,可以通过unsafe.SizeOf
函数来计算占用的字节大小。
空结构体的使用场景有很多
- 作为
map
的值类型,可以将map
作为set
来进行使用。 - 作为通道的类型,即代表一个不发送数据的通道。
- 。。。。。。
func main() {
type Empty struct {
}
fmt.Println(unsafe.Sizeof(Empty{}))
}
>>>>
0
内容参考自:
本文来自博客园,作者:ヾ(o◕∀◕)ノヾ,转载请注明原文链接:https://www.cnblogs.com/Jupiter-blog/p/18299009
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了