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并不等同于其他语言的nullnil仅仅只是一些类型的零值,并且不属于任何类型,

所以nil == nil这样的语句是无法通过编译的。

常量

常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:

  • 字面量

  • 其他常量标识符

  • 常量表达式

  • 结果是常量的类型转换

    • 例如,const MyConstant = int32(42):将整数 42 显式转换为 int32 类型,并将其赋值给常量 MyConstant
  • 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

标签语句,给一个代码块打上标签,可以是gotobreakcontinue的目标

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的键,无法覆盖,还会导致存在多个

任何数字都不等于NaNNaN也不等于自身,这也造成了每次哈希值都不相同

删除

删除一个键值对需要用到内置函数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

内容参考自:

posted @   ヾ(o◕∀◕)ノヾ  阅读(10)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示