在进入今天的主题前我们先来看一个小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语言,包括后续会学到的结构体及其字段,方法,自定义类型,接口等等。
标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:
补充:这里每集一些常见的关键字
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
,那么它就是取反运算符。statement
,并且规定了只能位于操作数的后方,所以不用再去纠结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
我们可以看出nil
仅仅是一个变量,Go中的nil
并不等同于其他语言的null
,nil
仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil
这样的语句是无法通过编译的。
常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:
常量只能是基本数据类型,不能是
常量的声明要用到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
是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。
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
是新声明的。
总结一下:变量的赋值有以下几种
在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())
}
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
语句可以在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 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)
}
标签语句,给一个代码块打上标签,可以是goto
,break
,continue
的目标。例子如下:
func main() {
A:
a := 1
B:
b := 2
}
单纯的使用标签是没有任何意义的,需要结合其他关键字来进行使用。
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
理解时它比较像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语言,包括后续会学到的结构体及其字段,方法,自定义类型,接口等等。
标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:
补充:这里每集一些常见的关键字
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
,那么它就是取反运算符。statement
,并且规定了只能位于操作数的后方,所以不用再去纠结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
我们可以看出nil
仅仅是一个变量,Go中的nil
并不等同于其他语言的null
,nil
仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil
这样的语句是无法通过编译的。
常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:
常量只能是基本数据类型,不能是
常量的声明要用到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
是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。
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
是新声明的。
总结一下:变量的赋值有以下几种
在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())
}
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
语句可以在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 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)
}
标签语句,给一个代码块打上标签,可以是goto
,break
,continue
的目标。例子如下:
func main() {
A:
a := 1
B:
b := 2
}
单纯的使用标签是没有任何意义的,需要结合其他关键字来进行使用。
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
理解时它比较像c++中的迭代器,以更加方便的遍历一些可迭代的数据结构,例如:数组,切片,字符串,映射表,通道,我们来看下面一个demo:
package main
import "fmt"
func main() {
sequence := "hello world"
for index, value := range sequence {
fmt.Printf("%d %c\n", index, value)
}
}
输出为: