go语言入门之路——基础语法

语法基础

前言

在进入今天的主题前我们先来看一个小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语言与其他语言的语言大差不差,故不作赘述,仅补充几点:

  1. go语言中没有选择将~作为取反运算符,而是复用了^符号,当两个数字使用^时,例如a^b,它就是异或运算符,只对一个数字使用时,例如^a,那么它就是取反运算符。
  2. Go语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++++i这样的问题。
  3. 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并不等同于其他语言的nullnil仅仅只是一些类型的零值,并且不属于任何类型,所以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是新声明的。

总结一下:变量的赋值有以下几种

  1. 声明时直接赋值
  2. 短变量初始化,去让编译器自动识别
    • 短变量在推断完以后,后续赋值不能改变其类型
    • 短变量类型初始化不能使用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提供的内置minmax函数只支持浮点数,到了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()) 
}

运行结果如下:

image-20240125215704353 写同理:
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

标签语句,给一个代码块打上标签,可以是gotobreakcontinue的目标。例子如下:

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语言与其他语言的语言大差不差,故不作赘述,仅补充几点:

  1. go语言中没有选择将~作为取反运算符,而是复用了^符号,当两个数字使用^时,例如a^b,它就是异或运算符,只对一个数字使用时,例如^a,那么它就是取反运算符。
  2. Go语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++++i这样的问题。
  3. 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并不等同于其他语言的nullnil仅仅只是一些类型的零值,并且不属于任何类型,所以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是新声明的。

总结一下:变量的赋值有以下几种

  1. 声明时直接赋值
  2. 短变量初始化,去让编译器自动识别
    • 短变量在推断完以后,后续赋值不能改变其类型
    • 短变量类型初始化不能使用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提供的内置minmax函数只支持浮点数,到了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()) 
}

运行结果如下:

image-20240125215704353

写同理:

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

标签语句,给一个代码块打上标签,可以是gotobreakcontinue的目标。例子如下:

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)
	}
}

输出为:
go语言入门之路——基础语法_第1张图片

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)
	}
}

输出为:

go语言入门之路——基础语法_第2张图片

你可能感兴趣的:(Go,golang,开发语言,后端)