语法基础
包
前言
在进入今天的主题前我们先来看一个小demo:
package main import "fmt" func main() { fmt.Println("Hello 世界!") }
注解:
package
关键字代表的是当前go文件属于哪一个包,启动文件通常是main
包,启动函数是main
函数,在自定义包和函数时命名应当尽量避免与之重复。
import
是导入关键字,后面跟着的是被导入的包名。
func
是函数声明关键字,用于声明一个函数。
fmt.Println("Hello 世界!")
是一个语句,调用了fmt
包下的Println
函数进行控制台
包的定义
在go语言中,程序一般是通过将包连接在一起来构建的,我们也可以理解成在go语言里面最基本的调用单位是包,而不是go文件,而包本质上就是一个文件夹,包内共享所有源文件的变量,常量,函数以及其他类型。
备注:包的命名风格建议都是小写字母且尽量简短
包的导入
我们接下来利用一个简单的小案例来介绍一下如何导入一个包:
首先我们创建exam文件夹,这时候我们需要在终端下输入以下命令来初始化exam文件夹作为exam包
go mod init exam go mod tidy
当exam文件夹中出现go.mod
文件时,说明文件夹已经初始化成功
这时候我们尝试写下面这样一个程序:
package exam import "fmt" func HelloWorld() { fmt.Println("Hello, world!") }
这时候我们尝试在main.go
文件中导入exam包:
package main import "exam" func main(){ exam.HelloWorld(); }
运行:
当然我们还可以尝试去给包起一个别名,比如这样:
package main import e "exam" func main(){ e.HelloWorld(); }
我们还可以导入多个包,如下面这样:
package main import (e "exam" d "demo") func main(){ e.HelloWorld(); d.Hellogo(); }
如果我们只是导入一个包,而不打算去调用其中的函数,可以采取匿名导入包的方式:
package main import (e "exam" _"demo") func main(){ e.HelloWorld(); }
当我们象采取这种方式的时候,一般是我们像调用包里面的init
函数
备注:什么是init()
函数
在Go语言中,import
语句用于导入其他包。当导入了一个包之后,并不会自动执行该包下的代码。但是,如果被导入的包中定义了init
函数,那么在导入时会自动调用该包中的init
函数。
init
函数在Go语言中具有特殊的用途。每个包可以包含一个或多个init
函数,它们在程序启动时自动执行,而不需要显式调用。当导入包时,Go编译器会首先执行该包下的init
函数。
init
函数的特点如下:
init
函数没有参数也没有返回值。init
函数在程序执行过程中不可被调用。init
函数在包级别中执行,并按照导入的顺序执行。即使包被导入多次,init
函数也只会执行一次。
通过导入包并触发该包下的init
函数,可以执行一些初始化操作,如初始化全局变量、注册服务、配置环境等。这样做的好处是可以保持包的独立性,并在程序启动时自动执行必要的初始化步骤,而无需手动调用。
禁止循环导入包
在Go中完全禁止循环导入,不管是直接的还是间接的。例如包A导入了包B,包B也导入了包A,这是直接循环导入,包A导入了包C,包C导入了包B,包B又导入了包A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。
包的导出
在Go中,导出和访问控制是通过命名来进行实现的,如果想要对外暴露一个函数或者一个变量,只需要将其名称首字母大写即可,例如example
包下的SayHello
函数。
package example import "fmt" // 首字母小写,外界无法访问 func sayHello() { fmt.Println("Hello") }
如果想要不对外暴露的话,只需将名称首字母改为小写即可,例如下方代码:
package example import "fmt" // 首字母小写,外界无法访问 func sayHello() { fmt.Println("Hello") }
注意:
对外暴露的函数和变量可以被包外的调用者导入和访问,如果是不对外暴露的话,那么仅包内的调用者可以访问,外部将无法导入和访问,该规则适用于整个Go语言,包括后续会学到的结构体及其字段,方法,自定义类型,接口等等。
标识符
标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:
- 只能由字母,数字,下划线组成
- 只能以字母和下划线开头
- 严格区分大小写
- 不能与任何已存在的标识符重复,即包内唯一的存在
- 不能与Go任何内置的关键字冲突
补充:这里每集一些常见的关键字
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语言与其他语言的语言大差不差,故不作赘述,仅补充几点:
- go语言中没有选择将
~
作为取反运算符,而是复用了^
符号,当两个数字使用^
时,例如a^b
,它就是异或运算符,只对一个数字使用时,例如^a
,那么它就是取反运算符。 - Go语言中没有自增与自减运算符,它们被降级为了语句
statement
,并且规定了只能位于操作数的后方,所以不用再去纠结i++
和++i
这样的问题。 - i++不再具有返回值,类似于
a = b++
这类语句的写法是错误的。
数据类型
布尔类型
布尔类型只有真值和假值。
类型 | 描述 |
bool | true 为真值,false 为假值 |
注意:在Go中,整数0并不代表假值,非零整数也不能代表真值,即数字无法代替布尔值进行逻辑判断,两者是完全不同的类型。
整形
序号 | 类型与描述 |
uint8 |
无符号 8 位整型 |
uint16 |
无符号 16 位整型 |
uint32 |
无符号 32 位整型 |
uint64 |
无符号 64 位整型 |
int8 |
有符号 8 位整型 |
int16 |
有符号 16 位整型 |
int32 |
有符号 32 位整型 |
int64 |
有符号 64 位整型 |
uint |
无符号整型 至少32位 |
int |
整型 至少32位 |
uintptr |
等价于无符号64位整型,但是专用于存放指针运算,用于存放死的指针地址。 |
浮点型
类型 | 类型和描述 |
float32 |
IEEE-754 32位浮点数 |
float64 |
IEEE-754 64位浮点数 |
字符类型
类型 | 类型和描述 |
byte |
等价 uint8 可以表达ANSCII字符 |
rune |
等价 int32 可以表达Unicode字符 |
string |
字符串即字节序列,可以转换为[]byte 类型即字节切片 |
零值
类型 | 零值 |
数字类型 | 0 |
布尔类型 | false |
字符串类型 | "" |
数组 | 固定长度的对应类型的零值集合 |
结构体 | 内部字段都是零值的结构体 |
切片,映射表,函数,接口,通道,指针 | nil |
注意:
我们来看一下源代码里面的 nil
var nil Type • 1
我们可以看出nil
仅仅是一个变量,Go中的nil
并不等同于其他语言的null
,nil
仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil
这样的语句是无法通过编译的。
常量
前言
常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:
- 字面量
- 其他常量标识符
- 常量表达式
- 结果是常量的类型转换
- iota
常量只能是基本数据类型,不能是
- 除基本类型以外的其它类型,如结构体,接口,切片,数组等
- 函数的返回值
常量的初始化
常量的声明要用到const
关键字,常量在声明式一定要赋上一个值,而且常量的类型可以省略,如下:
const a int =1 const s="fengxu" const numExpression = (1+2+3) / 2 % 100 + num // 常量表达式
常量的声明还可以批量化:
const( a=1; b=2; ) const( name="fengxu" sex="man" )
批量声明常量可以用()
括起来以提升可读性,可以存在多个()
达到分组的效果。
在同一个常量分组中,在已经赋值的常量后面的常量可以不用赋值,其值默认就是前一个的值。
iota
iota
是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。
const iota = 0
看几个使用案例
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 )
通过上面几个例子可以发现,iota
是递增的,第一个常量使用iota
值的表达式,根据序号值的变化会自动的赋值给后续的常量,直到用新的iota
重置,这个序号其实就是代码的相对行号,是相对于当前分组的起始行号.
变量
变量的声明
在go中的类型声明是后置的,变量的声明会用到var
关键字,格式为var 变量名 类型名
,变量名的命名规则必须遵守标识符的命名规则。如下:
var num int; var name string var char byte
当声明多个相同类型的变量时,可以只写一次类型
var a,b,c int;
当我们声明多个不同类型的变量时,可以使用()进行包裹,并且允许存在多个()
var ( name string age int address string ) var ( school string class int )
变量的赋值
赋值会用到运算符=
,例如
var name string name = "jack"
也可以声明的时候直接赋值
var name string = "jack"
或者这样也可以
var name string var age int name, age = "jack", 1
第二种方式每次都要指定类型,可以使用官方提供的语法糖:短变量初始化,可以省略掉var
关键字和后置类型,具体是什么类型交给编译器自行推断。
name := "jack" // 字符串类型的变量。
虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种代码无法通过编译。
a := 1 a = "1"
还需要注意的是,短变量初始化不能使用nil
,因为nil
不属于任何类型,编译器无法推断其类型。
name := nil // 无法通过编译
短变量声明可以批量初始化
name, age := "jack", 1
短变量声明方式无法对一个已存在的变量使用,比如
// 错误示例 var a int a := 1 // 错误示例 a := 1 a := 2
但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如
a := 1 a, b := 2, 2
这种代码是可以通过编译的,变量a
被重新赋值,而b
是新声明的。
总结一下:变量的赋值有以下几种
- 声明时直接赋值
- 短变量初始化,去让编译器自动识别
- 短变量在推断完以后,后续赋值不能改变其类型
- 短变量类型初始化不能使用nil
- 短变量声明可以批量初始化
- 短变量初始化只有在和新变量一起的时候才能对已经存在的变量使用
变量的交换
在Go中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下
num1, num2 := 25, 36 nam1, num2 = num2, num1
三个变量也是同样如此
num1, num2, num3 := 25, 36, 49 nam1, num2, num3 = num3, num2, num1
由于在函数内部存在未使用的变量会无法通过编译,但有些变量又确实用不到,这个时候就可以使用匿名变量_
,使用_
来表示该变量可以忽略,例如
a, b, _ := 1, 2, 3
变量的比较
变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go语言中不存在隐式类型转换,像下面这样的代码是无法通过编译的
func main() { var a uint64 var b int64 fmt.Println(a == b) }
编译器会告诉你两者之间类型并不相同
invalid operation: a == b (mismatched types uint64 and int64)
所以必须使用强制类型转换
func main() { var a uint64 var b int64 fmt.Println(int64(a) == b) }
在没有泛型之前,早期go提供的内置min
,max
函数只支持浮点数,到了1.21版本,go才终于将这两个内置函数用泛型重写。使用min
函数比较最小值
minVal := min(1, 2, -1, 1.2)
使用max
函数比较最大值
maxVal := max(100, 22, -1, 1.12)
它们的参数支持所有的可比较类型,go中的可比较类型有
- 布尔
- 数字
- 字符串
- 指针
- 通道 (仅支持判断是否相等)
- 元素是可比较类型的数组(切片不可比较)
- 字段类型都是可比较类型的结构体(仅支持判断是否相等)
除此之外,还可以通过导入标准库cmp
来判断,不过仅支持有序类型的参数,在go中内置的有序类型只有数字和字符串。
import "cmp" func main() { cmp.Compare(1, 2) cmp.Less(1, 2) }
输入与输出
标准
我们知道程序得输入与输出离不开os
模块,在os
包下有三给外包楼得文件描述符,其类型都是*File
,
如下:
var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") )
Stdin
- 标准输入Stdout
- 标准输出Stderr
- 标准错误
Go中的控制台输入输出都离不开它们。
输出
输出一句Hello 世界!
,比较常用的有三种方法,第一种是调用os.Stdout
os.Stdout.WriteString("Hello 世界!")
第二种是使用内置函数println
println("Hello 世界!")
第三种也是最推荐的一种就是调用fmt
包下的Println
函数
fmt.Println("Hello 世界!")
fmt.Println
会用到反射,因此输出的内容通常更容易使人阅读,不过性能很差强人意。
输入
输入的话是通常使用fmt
包下提供的三个函数
// 扫描从os.Stdin读入的文本,根据空格分隔,换行也被当作空格 func Scan(a ...any) (n int, err error) // 与Scan类似,但是遇到换行停止扫描 func Scanln(a ...any) (n int, err error) // 根据格式化的字符串扫描 func Scanf(format string, a ...any) (n int, err error)
缓冲
当对性能有要求时可以使用bufio
包进行读写,例如下面这个读的例子:
package main import ( "fmt" "os" "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
expression
必须是一个布尔表达式,即结果要么为真要么为假,必须是一个布尔值,例子如下:
func main() { a, b := 1, 2 if a > b { b++ } else { a++ } }
同时if
语句也可以包含一些简单的语句,例如:
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 { }
在执行的过程中每一个表达式的判断是从左到右,整个if
语句的判断是从上到下 。一个根据成绩打分的例子如下,第一种写法
func main() { score := 90 var ans string if score == 100 { ans = "S" } else if score >= 90 && score < 100 { ans = "A" } else if score >= 80 && score < 90 { ans = "B" } else if score >= 70 && score < 80 { ans = "C" } else if score >= 60 && score < 70 { ans = "E" } else if score >= 0 && score < 60 { ans = "F" } else { ans = "nil" } fmt.Println(ans) }
第二种写法利用了if
语句是从上到下的判断的前提,所以代码要更简洁些。
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() { str := "a" switch str { case "a": str += "a" str += "c" case "b": str += "bb" str += "aaaa" default: // 当所有case都不匹配后,就会执行default分支 str += "CCCC" } fmt.Println(str) }
还可以在表达式之前编写一些简单语句,例如声明新变量
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
语句也可以没有入口处的表达式。
func main() { num := 2 switch { // 等价于 switch true { case num >= 0 && num <= 1: num++ case num > 1: num-- case num < 0: num *= num } fmt.Println(num) }
通过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
将控制权传递给在同一函数中对应标签的语句,示例如下:
func main() { a := 1 if a == 1 { goto A } else { fmt.Println("b") } A: fmt.Println("a") }
在实际应用中goto
用的很少,跳来跳去的很降低代码可读性,性能消耗也是一个问题。
循环控制
前言
在Go中,仅有一种循环语句:for
,Go抛弃了while
语句,for
语句可以被当作while
来使用。
我们来看一个实例代码
package main import ( "fmt" ) func main(){ for i:=1;i<=10;i++{ fmt.Printf("%d\n",i) } }
for range
我个人对for range
理解时它比较像c++中的迭代器,以更加方便的遍历一些可迭代的数据结构,例如:数组,切片,字符串,映射表,通道,我们来看下面一个demo:
package main import "fmt" func main() { sequence := "hello world" for index, value := range sequence { fmt.Printf("%d %c\n", index, value) } }
输出为:
语法基础
包
前言
在进入今天的主题前我们先来看一个小demo:
package main import "fmt" func main() { fmt.Println("Hello 世界!") }
注解:
package
关键字代表的是当前go文件属于哪一个包,启动文件通常是main
包,启动函数是main
函数,在自定义包和函数时命名应当尽量避免与之重复。
import
是导入关键字,后面跟着的是被导入的包名。
func
是函数声明关键字,用于声明一个函数。
fmt.Println("Hello 世界!")
是一个语句,调用了fmt
包下的Println
函数进行控制台
包的定义
在go语言中,程序一般是通过将包连接在一起来构建的,我们也可以理解成在go语言里面最基本的调用单位是包,而不是go文件,而包本质上就是一个文件夹,包内共享所有源文件的变量,常量,函数以及其他类型。
备注:包的命名风格建议都是小写字母且尽量简短
包的导入
我们接下来利用一个简单的小案例来介绍一下如何导入一个包:
首先我们创建exam文件夹,这时候我们需要在终端下输入以下命令来初始化exam文件夹作为exam包
go mod init exam go mod tidy
当exam文件夹中出现go.mod
文件时,说明文件夹已经初始化成功
这时候我们尝试写下面这样一个程序:
package exam import "fmt" func HelloWorld() { fmt.Println("Hello, world!") }
这时候我们尝试在main.go
文件中导入exam包:
package main import "exam" func main(){ exam.HelloWorld(); }
当然我们还可以尝试去给包起一个别名,比如这样:
package main import e "exam" func main(){ e.HelloWorld(); }
我们还可以导入多个包,如下面这样:
package main import (e "exam" d "demo") func main(){ e.HelloWorld(); d.Hellogo(); }
如果我们只是导入一个包,而不打算去调用其中的函数,可以采取匿名导入包的方式:
package main import (e "exam" _"demo") func main(){ e.HelloWorld(); }
当我们象采取这种方式的时候,一般是我们像调用包里面的init
函数
备注:什么是init()
函数
在Go语言中,import
语句用于导入其他包。当导入了一个包之后,并不会自动执行该包下的代码。但是,如果被导入的包中定义了init
函数,那么在导入时会自动调用该包中的init
函数。
init
函数在Go语言中具有特殊的用途。每个包可以包含一个或多个init
函数,它们在程序启动时自动执行,而不需要显式调用。当导入包时,Go编译器会首先执行该包下的init
函数。
init
函数的特点如下:
init
函数没有参数也没有返回值。init
函数在程序执行过程中不可被调用。init
函数在包级别中执行,并按照导入的顺序执行。即使包被导入多次,init
函数也只会执行一次。
通过导入包并触发该包下的init
函数,可以执行一些初始化操作,如初始化全局变量、注册服务、配置环境等。这样做的好处是可以保持包的独立性,并在程序启动时自动执行必要的初始化步骤,而无需手动调用。
禁止循环导入包
在Go中完全禁止循环导入,不管是直接的还是间接的。例如包A导入了包B,包B也导入了包A,这是直接循环导入,包A导入了包C,包C导入了包B,包B又导入了包A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。
包的导出
在Go中,导出和访问控制是通过命名来进行实现的,如果想要对外暴露一个函数或者一个变量,只需要将其名称首字母大写即可,例如example
包下的SayHello
函数。
package example import "fmt" // 首字母小写,外界无法访问 func sayHello() { fmt.Println("Hello") }
如果想要不对外暴露的话,只需将名称首字母改为小写即可,例如下方代码:
package example import "fmt" // 首字母小写,外界无法访问 func sayHello() { fmt.Println("Hello") }
注意:
对外暴露的函数和变量可以被包外的调用者导入和访问,如果是不对外暴露的话,那么仅包内的调用者可以访问,外部将无法导入和访问,该规则适用于整个Go语言,包括后续会学到的结构体及其字段,方法,自定义类型,接口等等。
标识符
标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:
- 只能由字母,数字,下划线组成
- 只能以字母和下划线开头
- 严格区分大小写
- 不能与任何已存在的标识符重复,即包内唯一的存在
- 不能与Go任何内置的关键字冲突
补充:这里每集一些常见的关键字
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语言与其他语言的语言大差不差,故不作赘述,仅补充几点:
- go语言中没有选择将
~
作为取反运算符,而是复用了^
符号,当两个数字使用^
时,例如a^b
,它就是异或运算符,只对一个数字使用时,例如^a
,那么它就是取反运算符。 - Go语言中没有自增与自减运算符,它们被降级为了语句
statement
,并且规定了只能位于操作数的后方,所以不用再去纠结i++
和++i
这样的问题。 - i++不再具有返回值,类似于
a = b++
这类语句的写法是错误的。
数据类型
布尔类型
布尔类型只有真值和假值。
类型 | 描述 |
bool | true 为真值,false 为假值 |
注意:在Go中,整数0并不代表假值,非零整数也不能代表真值,即数字无法代替布尔值进行逻辑判断,两者是完全不同的类型。
整形
序号 | 类型与描述 |
uint8 |
无符号 8 位整型 |
uint16 |
无符号 16 位整型 |
uint32 |
无符号 32 位整型 |
uint64 |
无符号 64 位整型 |
int8 |
有符号 8 位整型 |
int16 |
有符号 16 位整型 |
int32 |
有符号 32 位整型 |
int64 |
有符号 64 位整型 |
uint |
无符号整型 至少32位 |
int |
整型 至少32位 |
uintptr |
等价于无符号64位整型,但是专用于存放指针运算,用于存放死的指针地址。 |
浮点型
类型 | 类型和描述 |
float32 |
IEEE-754 32位浮点数 |
float64 |
IEEE-754 64位浮点数 |
字符类型
类型 | 类型和描述 |
byte |
等价 uint8 可以表达ANSCII字符 |
rune |
等价 int32 可以表达Unicode字符 |
string |
字符串即字节序列,可以转换为[]byte 类型即字节切片 |
零值
类型 | 零值 |
数字类型 | 0 |
布尔类型 | false |
字符串类型 | "" |
数组 | 固定长度的对应类型的零值集合 |
结构体 | 内部字段都是零值的结构体 |
切片,映射表,函数,接口,通道,指针 | nil |
注意:
我们来看一下源代码里面的 nil
var nil Type • 1
我们可以看出nil
仅仅是一个变量,Go中的nil
并不等同于其他语言的null
,nil
仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil
这样的语句是无法通过编译的。
常量
前言
常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:
- 字面量
- 其他常量标识符
- 常量表达式
- 结果是常量的类型转换
- iota
常量只能是基本数据类型,不能是
- 除基本类型以外的其它类型,如结构体,接口,切片,数组等
- 函数的返回值
常量的初始化
常量的声明要用到const
关键字,常量在声明式一定要赋上一个值,而且常量的类型可以省略,如下:
const a int =1 const s="fengxu" const numExpression = (1+2+3) / 2 % 100 + num // 常量表达式
常量的声明还可以批量化:
const( a=1; b=2; ) const( name="fengxu" sex="man" )
批量声明常量可以用()
括起来以提升可读性,可以存在多个()
达到分组的效果。
在同一个常量分组中,在已经赋值的常量后面的常量可以不用赋值,其值默认就是前一个的值。
iota
iota
是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。
const iota = 0
看几个使用案例
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 )
通过上面几个例子可以发现,iota
是递增的,第一个常量使用iota
值的表达式,根据序号值的变化会自动的赋值给后续的常量,直到用新的iota
重置,这个序号其实就是代码的相对行号,是相对于当前分组的起始行号.
变量
变量的声明
在go中的类型声明是后置的,变量的声明会用到var
关键字,格式为var 变量名 类型名
,变量名的命名规则必须遵守标识符的命名规则。如下:
var num int; var name string var char byte
当声明多个相同类型的变量时,可以只写一次类型
var a,b,c int;
当我们声明多个不同类型的变量时,可以使用()进行包裹,并且允许存在多个()
var ( name string age int address string ) var ( school string class int )
变量的赋值
赋值会用到运算符=
,例如
var name string name = "jack"
也可以声明的时候直接赋值
var name string = "jack"
或者这样也可以
var name string var age int name, age = "jack", 1
第二种方式每次都要指定类型,可以使用官方提供的语法糖:短变量初始化,可以省略掉var
关键字和后置类型,具体是什么类型交给编译器自行推断。
name := "jack" // 字符串类型的变量。
虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种代码无法通过编译。
a := 1 a = "1"
还需要注意的是,短变量初始化不能使用nil
,因为nil
不属于任何类型,编译器无法推断其类型。
name := nil // 无法通过编译
短变量声明可以批量初始化
name, age := "jack", 1
短变量声明方式无法对一个已存在的变量使用,比如
// 错误示例 var a int a := 1 // 错误示例 a := 1 a := 2
但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如
a := 1 a, b := 2, 2
这种代码是可以通过编译的,变量a
被重新赋值,而b
是新声明的。
总结一下:变量的赋值有以下几种
- 声明时直接赋值
- 短变量初始化,去让编译器自动识别
- 短变量在推断完以后,后续赋值不能改变其类型
- 短变量类型初始化不能使用nil
- 短变量声明可以批量初始化
- 短变量初始化只有在和新变量一起的时候才能对已经存在的变量使用
变量的交换
在Go中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下
num1, num2 := 25, 36 nam1, num2 = num2, num1
三个变量也是同样如此
num1, num2, num3 := 25, 36, 49 nam1, num2, num3 = num3, num2, num1
由于在函数内部存在未使用的变量会无法通过编译,但有些变量又确实用不到,这个时候就可以使用匿名变量_
,使用_
来表示该变量可以忽略,例如
a, b, _ := 1, 2, 3
变量的比较
变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go语言中不存在隐式类型转换,像下面这样的代码是无法通过编译的
func main() { var a uint64 var b int64 fmt.Println(a == b) }
编译器会告诉你两者之间类型并不相同
invalid operation: a == b (mismatched types uint64 and int64)
所以必须使用强制类型转换
func main() { var a uint64 var b int64 fmt.Println(int64(a) == b) }
在没有泛型之前,早期go提供的内置min
,max
函数只支持浮点数,到了1.21版本,go才终于将这两个内置函数用泛型重写。使用min
函数比较最小值
minVal := min(1, 2, -1, 1.2)
使用max
函数比较最大值
maxVal := max(100, 22, -1, 1.12)
它们的参数支持所有的可比较类型,go中的可比较类型有
- 布尔
- 数字
- 字符串
- 指针
- 通道 (仅支持判断是否相等)
- 元素是可比较类型的数组(切片不可比较)
- 字段类型都是可比较类型的结构体(仅支持判断是否相等)
除此之外,还可以通过导入标准库cmp
来判断,不过仅支持有序类型的参数,在go中内置的有序类型只有数字和字符串。
import "cmp" func main() { cmp.Compare(1, 2) cmp.Less(1, 2) }
输入与输出
标准
我们知道程序得输入与输出离不开os
模块,在os
包下有三给外包楼得文件描述符,其类型都是*File
,
如下:
var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") )
Stdin
- 标准输入Stdout
- 标准输出Stderr
- 标准错误
Go中的控制台输入输出都离不开它们。
输出
输出一句Hello 世界!
,比较常用的有三种方法,第一种是调用os.Stdout
os.Stdout.WriteString("Hello 世界!")
第二种是使用内置函数println
println("Hello 世界!")
第三种也是最推荐的一种就是调用fmt
包下的Println
函数
fmt.Println("Hello 世界!")
fmt.Println
会用到反射,因此输出的内容通常更容易使人阅读,不过性能很差强人意。
输入
输入的话是通常使用fmt
包下提供的三个函数
// 扫描从os.Stdin读入的文本,根据空格分隔,换行也被当作空格 func Scan(a ...any) (n int, err error) // 与Scan类似,但是遇到换行停止扫描 func Scanln(a ...any) (n int, err error) // 根据格式化的字符串扫描 func Scanf(format string, a ...any) (n int, err error)
缓冲
当对性能有要求时可以使用bufio
包进行读写,例如下面这个读的例子:
package main import ( "fmt" "os" "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
expression
必须是一个布尔表达式,即结果要么为真要么为假,必须是一个布尔值,例子如下:
func main() { a, b := 1, 2 if a > b { b++ } else { a++ } }
同时if
语句也可以包含一些简单的语句,例如:
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 { }
在执行的过程中每一个表达式的判断是从左到右,整个if
语句的判断是从上到下 。一个根据成绩打分的例子如下,第一种写法
func main() { score := 90 var ans string if score == 100 { ans = "S" } else if score >= 90 && score < 100 { ans = "A" } else if score >= 80 && score < 90 { ans = "B" } else if score >= 70 && score < 80 { ans = "C" } else if score >= 60 && score < 70 { ans = "E" } else if score >= 0 && score < 60 { ans = "F" } else { ans = "nil" } fmt.Println(ans) }
第二种写法利用了if
语句是从上到下的判断的前提,所以代码要更简洁些。
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() { str := "a" switch str { case "a": str += "a" str += "c" case "b": str += "bb" str += "aaaa" default: // 当所有case都不匹配后,就会执行default分支 str += "CCCC" } fmt.Println(str) }
还可以在表达式之前编写一些简单语句,例如声明新变量
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
语句也可以没有入口处的表达式。
func main() { num := 2 switch { // 等价于 switch true { case num >= 0 && num <= 1: num++ case num > 1: num-- case num < 0: num *= num } fmt.Println(num) }
通过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
将控制权传递给在同一函数中对应标签的语句,示例如下:
func main() { a := 1 if a == 1 { goto A } else { fmt.Println("b") } A: fmt.Println("a") }
在实际应用中goto
用的很少,跳来跳去的很降低代码可读性,性能消耗也是一个问题。
循环控制
前言
在Go中,仅有一种循环语句:for
,Go抛弃了while
语句,for
语句可以被当作while
来使用。
我们来看一个实例代码
package main import ( "fmt" ) func main(){ for i:=1;i<=10;i++{ fmt.Printf("%d\n",i) } }
输出为:
for range
我个人对for range
理解时它比较像c++中的迭代器,以更加方便的遍历一些可迭代的数据结构,例如:数组,切片,字符串,映射表,通道,我们来看下面一个demo:
package main import "fmt" func main() { sequence := "hello world" for index, value := range sequence { fmt.Printf("%d %c\n", index, value) } }
输出为: