为什么选择Golang

1. Go语言上手简单

Go语言语法简单易懂,学习曲线平缓,不需要像 C/ C++ 语言动辄需要两到三年的学习期。 一个熟练的开发者只需要短短的一周时间就可以从学习阶段转到开发阶段,并完成一个高并发的服务器开发。

Go语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。

Go语言在拥有一些动态语言的特性的同时,其语法风格类似于C语言。在C语言的基础上进行了大幅的简化,去掉了不需要的表达式括号,循环也只有 for 一种表示方法,就可以实现数值、键值等各种遍历。因此,Go语言上手非常容易。

那么,Go语言到底有多么简单?下面通过实现一个 HTTP 服务器来了解一下。

【实例】HTTP 文件服务器是常见的 Web 服务之一。开发阶段为了测试,需要自行安装 Apache 或 Nginx 服务器,下载安装配置需要大量的时间。使用Go语言实现一个简单的 HTTP 服务器只需要几行代码,如下所示。

package main//标记当前文件为 main 包,main 包也是 Go 程序的入口包。

import (//导入 net/http 包,这个包的作用是 HTTP 的基础封装和访问。
    "net/http"
)

func main() {//程序执行的入口函数 main()。
    http.Handle("/", http.FileServer(http.Dir(".")))
    http.ListenAndServe(":8080", nil)
}

2. Go语言工程结构简单

Go语言的源码无须头文件,编译的文件都来自于后缀名为 .go 的源码文件。

Go语言无须解决方案、工程文件和 Make File,只要将工程文件按照 GOPATH 的规则进行填充,即可使用 go build/go install 进行编译,编译完成的二进制可执行文件统一放在 bin 文件夹下。

3. Go语言编译速度快

Go语言可以利用自己的特性实现并发编译,并发编译的最小元素是包。从 Go 1.9 版本开始,最小并发编译元素缩小到函数,整体编译速度提高了 20%。

另外,Go语言语法简单,具有严谨的工程结构设计、没有头文件、不允许包的交叉依赖等规则,在很大程度上加速了编译的过程。

4. Go语言代码风格清晰、简单

C语言的有些语法会让代码可读性降低甚至发生歧义。Go语言在C语言的基础上取其精华,弃其糟粕,将C语言中较为容易发生错误的写法进行调整,做出相应的编译提示。

1.去掉循环冗余括号

Go语言在众多大师的丰富实战经验的基础上诞生,去除了C语言语法中一些冗余、烦琐的部分。下面的代码是C语言的数值循环:

// C语言的for数值循环
for(int a = 0;a<10;a++){
    // 循环代码
}

在Go语言中,这样的循环变为:

for a := 0;a<10;a++{
    // 循环代码
}

for 两边的括号被去掉,int 声明被简化为 := ,直接通过编译器右值推导获得 a 的变量类型并声明。

2. 去掉表达式冗余括号

同样的简化也可以在判断语句中体现出来,以下是C语言的判断语句:

if (表达式){
    // 表达式成立
}

在Go语言中,无须添加表达式括号,代码如下:

if 表达式{
    // 表达式成立
}

3.强制的代码风格

Go语言中,左括号必须紧接着语句不换行。其他样式的括号将被视为代码编译错误。这个特性刚开始会使开发者有一些不习惯,但随着对Go语言的不断熟悉,开发者就会发现风格统一让大家在阅读代码时把注意力集中到了解决问题上,而不是代码风格上。

同时Go语言也提供了一套格式化工具。一些Go语言的开发环境或者编辑器在保存时,都会使用格式化工具对代码进行格式化,让代码提交时已经是统一格式的代码。

4.不再纠结于 i++ 和 ++i

在Go语言中,自增操作符不再是一个操作符,而是一个语句。因此,在Go语言中自增只有一种写法:i++

如果写成前置自增 ++i ,或者赋值后自增 a=i++ 都将导致编译错误。


第一个Go语言程序

在控制台输出“Hello World!”

package main    // 声明 main 
//语法:package name,package为包的关键字,name为包名

import (
    "fmt"       // 导入 fmt 包,打印字符串是需要用到
)
//语法:import “name”,import为导入包的关键字,name为包名

func main() {   // 声明 main 主函数
    fmt.Println("Hello World!") // 打印 Hello World!
}

Go语言的包与文件夹是一一对应的,它具有以下几点特性:

  1. 一个目录下的同级文件属于同一个包。

  2. 包名可以与其目录名不同。

  3. main 包是Go语言程序的入口包,一个Go语言程序必须有且仅有一个 main 包。如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。

代码第 4 行导入了 fmt 包,这行代码会告诉 Go 编译器,我们需要用到 fmt 包中的函数或者变量等,fmt 包是Go语言标准库为我们提供的,用于格式化输入输出的内容(类似于C语言中的 stdio.h 头文件),类似的还有 os 包、io 包等。

另外有一点需要注意,导入的包中不能含有代码中没有使用到的包,否则Go编译器会报编译错误!

例如:imported and not used: "xxx" ,"xxx" 表示包名。

main 函数

代码的第 7 行创建了一个 main 函数,它是Go语言程序的入口函数,也即程序启动后运行的第一个函数。main 函数只能声明在 main 包中,不能声明在其他包中,并且,一个 main 包中也必须有且仅有一个 main 函数。C/ C++ 程序的入口函数也是 main(),一个 C/C++ 程序有且只能有一个 main() 函数。

main 函数是自定义函数的一种,在Go语言中,所有函数都以关键字 func 开头的,定义格式如下所示:

func 函数名 (参数列表) (返回值列表){

函数体

}

格式说明如下:

  1. 函数名:由字母、数字、下画线_组成,其中,函数名的第一个字母不能为数字,并且,在同一个包内,函数名称不能重名。

  2. 参数列表:一个参数由参数变量和参数类型组成,例如 func foo( a int, b string )

  3. 返回值列表:可以是返回值类型列表,也可以是参数列表那样变量名与类型的组合,函数有返回值时,必须在函数体中使用 return 语句返回。

  4. 函数体:能够被重复调用的代码片段。

注意:Go语言函数的左大括号 { 必须和函数名称在同一行,否则会报错。

代码的第 8 行 fmt.Println("Hello World!") 中,Println 是 fmt 包中的一个函数,它用来格式化输出数据,比如字符串、整数、小数等,类似于C语言中的 printf 函数。这里我们使用 Println 函数来打印字符串,也就是 ( ) 里面使用 "" 包裹的部分。注意,Println 函数打印完成后会自动换行,ln是 line 的缩写。

点号 . 是Go语言运算符的一种,这里表示调用 fmt 包中的 Println 函数。

另外,代码 fmt.Println("Hello World!") 的结尾,不需要使用 ; 来作为结束符,Go 编译器会自动帮我们添加,当然,在这里加上 ; 也是可以的。


第一章 Go语法

1.1 Go语言变量声明

  • 声明变量的一般形式是使用 var 关键字:
    var name type
    其中,var 是声明变量的关键字,name 是变量名,type 是变量的类型。

Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如: int* a, b; 。其中只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以和轻松地将它们都声明为指针类型:
var a, b *int

Go语言的基本类型有:

bool

string

int、int8、int16、int32、int64

uint、uint8、uint16、uint32、uint64、uintptr

byte // uint8 的别名

rune // int32 的别名 代表一个 Unicode 码

float32、float64

complex64、complex128

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的。

变量声明的几种方式

  1. 标准格式

Go语言的变量声明的标准格式为:var 变量名 变量类型

变量声明以关键字 var 开头,后置变量类型,行尾无须分号

  1. 批量格式
    每行都用 var 声明变量比较烦琐

var (

    a int

    b string

    c []float32

    d func() bool

    e struct {

        x int

    }

)
  1. 简短格式
    除 var 关键字外,还可使用更加简短的变量定义和初始化语法。

    名字 := 表达式
    需要注意的是,简短模式(short variable declaration)有以下限制:

  1. 定义变量,同时显式初始化。

  2. 不能提供数据类型。

  3. 只能用在函数内部。

1.2 Go语言中变量的初始化

对比C语言:
在C语言中,变量在声明时,并不会对变量对应内存区域进行清理操作。此时,变量值可能是完全不可预期的结果。开发者需要习惯在使用C语言进行声明时要初始化操作,稍有不慎,就会造成不可预知的后果。

小知识:

微软的 VC 编译器会将未初始化的栈空间以 16 进制的 0xCC 填充,而未初始化的堆空间使用 0xCD 填充,而 0xCCCC 和 0xCDCD 在中文的 GB2312 编码中刚好对应“烫”和“屯”字。因此,如果一个字符串没有结束符 \0 ,直接输出的内存数据转换为字符串就刚好对应“烫烫烫”和“屯屯屯”。

变量初始化的标准格式

var 变量名 类型 = 表达式

package main

import (
	"fmt"
)

func main() {
	var attack float64 = 300.0
	var rate float64 = 0.5
	var defence float64 = 10.0
	var damage float64 = rate * (attack - defence)
	fmt.Println("伤害:", damage)
}

注意:这里是变量声明+变量初始化。如果不声明变量类型,进行初始化,编译器会自动推导!!
如下:

package main

import (
	"fmt"
)
func main() {//声明+初始化变量
	var attack  = 300.0
	var rate = 0.5
	var defence = 10.0
	var damage = rate * (attack - defence)
	fmt.Println("伤害:", damage)

    fmt.Printf("attack: %T\n", attack)
    fmt.Printf("rate: %T\n", rate)
    fmt.Printf("defence: %T\n", defence)
    fmt.Printf("damage: %T\n", damage)
}

输出如下:

伤害: 145
attack: float64
rate: float64
defence: float64
damage: float64

变量只能先声明后使用,上面两种写法等同于下面这种写法,而且变量声明之后就不能重复声明,在go语言中,声明的变量必须被使用,否则编译器会报错

package main

import (
	"fmt"
)

func main() {
	var (//批量声明变量
		attack  float64
		rate    float64
		defence float64
		damage  float64
	)
     //初始化变量
	attack = 300.0
	rate = 0.5
	defence = 10.0
	damage = rate * (attack - defence)
	fmt.Println("伤害:", damage)
}

短变量声明并初始化

var 的变量声明还有一种更为精简的写法:

hp := 100//这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。

如果 hp 已经被声明过,但依然使用 := 时编译器会报错:

/

编译报错如下:no new variables on left side of :=

意思是,在“:=”的左边没有新变量出现,意思就是“:=”的左边变量已经被声明了。

短变量声明的形式在开发中的例子较多,比如:

conn, err := net.Dial("tcp","127.0.0.1:8080")

net.Dial 提供按指定协议和地址发起网络连接,这个函数有两个返回值,一个是连接对象(conn),一个是错误对象(err)。如果是标准格式将会变成:

var conn net.Conn
var err error
conn, err = net.Dial("tcp", "127.0.0.1:8080")

因此,短变量声明并初始化的格式在开发中使用比较普遍。

注意:在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错,代码如下:

conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")

1.3 Go语言多个变量同时赋值

交换变量:
错误示范

package main

import (
	"fmt"
)

func main() {

	//交换变量
	var front = 100
	var back = 150
	var temp int
	fmt.Println("交换前:")
	fmt.Println("front:", front)
	fmt.Println("back:", back)
	temp = front
	front = back
	back = temp
	fmt.Println("交换后:")
	fmt.Println("front:", front)
	fmt.Println("back:", back)
	fmt.Println("temp:", temp)
}

在Go语言中,内存不再是紧缺资源,而且写法可以更简单。使用 Go 的“多重赋值”特性,可以轻松完成变量交换的任务:

package main

import (
	"fmt"
)

func main() {
	var a = 100
	var b = 200
	fmt.Println("交换前:")
	fmt.Printf("a=%d\nb=%d\n", a, b)
	a, b = b, a
	fmt.Println("交换后:")
	fmt.Printf("a=%d\nb=%d\n", a, b)

}

输出:

交换前:
a=100
b=200
交换后:
a=200
b=100

1.4 Go语言匿名变量

在编码过程中,可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。

匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。

package main

import (
	"fmt"
)

func GetData() (int, int) {//这个函数有两个返回值
	return 100, 200
}
func main() {
	a, _ := GetData()//获取函数GetData的第一个返回值,第二个返回值由匿名变量接收(下划线)
	_, b := GetData()//获取函数GetData的第二个返回值,第一个返回值由匿名变量接收(下划线)
	fmt.Println(a, b)
}

匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

小知识:

在 Lua 等编程语言里,匿名变量也被叫做哑元变量。


1.4 Go语言变量的作用域

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。

了解变量的作用域对我们学习Go语言来说是比较重要的,因为Go语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。

根据变量定义位置的不同,可以分为以下三个类型:

函数内定义的变量称为局部变量

函数外定义的变量称为全局变量

函数定义中的变量称为形式参数

  1. 局部变量:

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。

局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。

package main

import (
	"fmt"
)

func main() {
 	//声明局部变量 a 和 b 并赋值
    var a int = 3
    var b int = 4
    //声明局部变量 c 并计算 a 和 b 的和
    c := a + b
    fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

输出:

a = 3, b = 4, c = 7
  1. 全局变量:

在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。

全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。

下面代码中,第 6 行定义了全局变量 c

package main

import "fmt"

//声明全局变量
var c int

func main() {
    //声明局部变量
    var a, b int

    //初始化参数
    a = 3
    b = 4
    c = a + b

    fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。

package main

import "fmt"

//声明全局变量
var a float32 = 3.14

func main() {
    //声明局部变量
    var a int = 3

    fmt.Printf("a = %d\n", a)
}

运行结果如下所示:a = 3

  1. 形式参数

在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。
形式参数会作为函数的局部变量来使用。

package main

import (
	"fmt"
)

// 全局变量 a
var a int = 13

func main() {
	//局部变量 a 和 b
	var a int = 3
	var b int = 4

	fmt.Printf("main() 函数中 a = %d\n", a)
	fmt.Printf("main() 函数中 b = %d\n", b)
	c := sum()
	fmt.Printf("main() 函数中 c = %d\n", c)
}

func sum() int {
	var b int = 8
	var a int = 9
	fmt.Printf("sum() 函数中 a = %d\n", a)
	fmt.Printf("sum() 函数中 b = %d\n", b)
	num := a + b
	return num
}

运行结果:

main() 函数中 a = 3
main() 函数中 b = 4
sum() 函数中 a = 9
sum() 函数中 b = 8
main() 函数中 c = 17

整数类型

1.1 规则陈述

Go 语言提供有符号整数 int8int16int32int64,无符号整数 uint8uint16uint32uint64,以及平台相关字长的 intuint。此外,runeint32 的别名,byteuint8 的别名,uintptr 用于底层指针运算。

有符号整数采用补码表示,n-bit 有符号整数取值范围为 −2n−1 2n−1−1 ,无符号整数为 0 2n−1

intuint 的具体大小取决于编译目标平台(32 位或 64 位),因此跨平台代码中处理二进制数据时应避免使用。

1.2 代码验证

正例:

go

package main

import "fmt"

func main() {
    var a int8 = 127       // int8 最大值
    var b uint8 = 255      // uint8 最大值
    var c rune = '中'      // rune = int32,存储 Unicode 码点
    var d byte = 65       // byte = uint8

    fmt.Println(a, b, c, d)
}

输出:

plain

127 255 20013 65

反例(编译错误):

go

var a int8 = 128     // 编译错误:constant 128 overflows int8
var b uint8 = -1    // 编译错误:constant -1 overflows uint8

类型转换必须显式:

go

var x int32 = 100
var y int64 = 200
var z = x + y        // 编译错误:invalid operation: x + y (mismatched types int32 and int64)

修正:

go

var z = int64(x) + y   // 显式转换

1.3 机制分析

Go 是强类型语言,即使 intint32 在 32 位平台上占用相同内存,它们仍是不同类型。这种设计消除了隐式转换带来的意外行为。例如 C 语言中 intlong 的隐式转换在不同平台上行为不一致,Go 通过强制显式转换避免了这一问题。

runebyte 作为类型别名,在类型系统中与 int32uint8 完全等价,可以互换使用。但 byterune 之间不能直接比较或运算:

go

var m byte = 65
var n rune = 'A'
fmt.Println(m == n)   // 编译错误:invalid operation: m == n (mismatched types byte and rune)

1.4 边界情况

intuint 的大小随平台变化,在需要固定大小的场景(如文件格式、网络协议)中必须使用 int32/int64 等明确大小的类型:

go

// 错误:文件结构在不同平台可能不同
type Header struct {
    Size int      // 可能是 32 位或 64 位
}

// 正确:确保跨平台一致性
type Header struct {
    Size int32    // 固定 32 位
}

1.5 实践规范

  • 通用计数、索引使用 int

  • 二进制数据、文件结构使用明确大小的类型(int32uint16 等)

  • 字符处理使用 rune,原始字节使用 byte

  • 不同类型间运算必须显式转换


浮点数类型

2.1 规则陈述

Go 提供 float32float64 两种浮点数类型,遵循 IEEE 754 标准。float32 提供约 6-7 位十进制精度,float64 提供约 15-16 位。浮点数字面量默认类型为 float64

math 包提供极值常量:math.MaxFloat32(约 3.4×1038 )、math.MaxFloat64(约 1.8×10308 )。

代码验证

package main

import (
    "fmt"
    "math"
)

func main() {
    var a float32 = math.MaxFloat32
    var b float64 = math.MaxFloat64

    fmt.Println(a)
    fmt.Println(b)

    // 科学计数法
    const Avogadro = 6.02214129e23
    fmt.Println(Avogadro)
}

输出:

3.4028235e+38
1.7976931348623157e+308
6.02214129e+23

精度陷阱:

var f float32 = 16777216
fmt.Println(f == f+1)   // true

float32 的尾数为 23 位,能精确表示的最大整数为 224=16777216 。在此值上加 1 时,由于精度不足,结果仍为 16777216。

var g float64 = 9007199254740992
fmt.Println(g == g+1)   // true

同理,float64 的尾数为 52 位,能精确表示的最大整数为 253=9007199254740992

类型不匹配:

var a = 3.14        // float64(默认)
var b float32 = a   // 编译错误:cannot use a (type float64) as type float32

修正:

var b float32 = float32(a)

2.3 机制分析

浮点数的精度由尾数位数决定。float32 的 23 位尾数对应约 7 位十进制精度,但由于十进制与二进制的转换损耗,实际可靠精度约为 6 位。float64 的 52 位尾数对应约 16 位十进制精度。

默认使用 float64 而非 float32 的原因在于:

  1. float32 的累积误差在多次运算后会显著扩散

  2. 现代 64 位 CPU 处理 float64float32 性能差异极小

  3. float64 能精确表示更大的整数范围

2.4 边界情况

浮点数与整数的混合运算:

var c int = 10
var d = c / 3.0     // 合法:int 隐式转为 float64,d 为 float64
fmt.Println(d)      // 3.3333333333333335

但反向不行:

var e = 3.0 / c     // 合法,e 为 float64
var f int = 3.0 / c // 编译错误:cannot convert 3.0 / c (type float64) to type int

2.5 实践规范

  • 优先使用 float64

  • 涉及金钱计算时避免使用浮点数,改用整数(如分而非元)

  • 比较浮点数时避免直接使用 ==,应判断差值是否小于某个 epsilon


3. 复数类型

Go 提供 complex64(由两个 float32 组成)和 complex128(由两个 float64 组成)。复数字面量默认类型为 complex128。内置函数 complex(x, y) 构造复数,real(z) 提取实部,imag(z) 提取虚部。

math/cmplx 包提供复数运算函数,参数类型均为 complex128

package main

import "fmt"

func main() {
    var x = complex(3, 4)   // 3+4i,complex128
    var y = complex(1, 2)   // 1+2i

    fmt.Println(x + y)      // (4+6i)
    fmt.Println(x * y)      // (-5+10i)
    fmt.Println(real(x))    // 3
    fmt.Println(imag(x))    // 4
    fmt.Println(x == complex(3, 4))  // true
}

使用 math/cmplx:

package main

import (
    "fmt"
    "math/cmplx"
)

func main() {
    var z complex128 = complex(3, 4)

    fmt.Println(cmplx.Abs(z))       // 5(模:sqrt(3^2 + 4^2))
    fmt.Println(cmplx.Conj(z))     // (3-4i)(共轭)
}

反例(类型不匹配):

var z complex64 = complex(1.5, 2.5)
mod := cmplx.Abs(z)    // 编译错误:cannot use z (type complex64) as type complex128

修正:

var z complex128 = complex(1.5, 2.5)   // 使用 complex128
mod := cmplx.Abs(z)

或显式转换:

mod := cmplx.Abs(complex128(z))

机制分析

复数在内存中连续存储两个浮点数,实部在前,虚部在后。complex128 占用 16 字节(两个 float64),complex64 占用 8 字节(两个 float32)。

math/cmplx 包统一使用 complex128 的原因与 math 包统一使用 float64 一致:避免精度损失,且 complex128 是默认类型。

复数相等比较 ==!= 要求实部和虚部同时相等。由于浮点数精度问题,直接比较可能产生意外结果:

var a = complex(0.1, 0.2)
var b = complex(0.3, 0.4)
var c = a + b
fmt.Println(c == complex(0.4, 0.6))   // false

原因在于 0.10.20.3 等十进制小数在二进制浮点数中无法精确表示,相加后的结果与 complex(0.4, 0.6) 存在微小差异。

边界情况

复数除法:

var z1 = complex(2, 3)
var z2 = complex(1, 4)
fmt.Println(z1 / z2)   // (0.8235294117647058-0.29411764705882354i)

除法公式为:

c+dia+bi=c2+d2(ac+bd)+(bcad)i

当除数的模接近零时,结果会趋向无穷大:

var z3 = complex(1, 2)
var z4 = complex(0, 0)
fmt.Println(z3 / z4)   // (+Inf+Infi)

实践规范

  • 优先使用 complex128,与 math/cmplx 包兼容

  • 复数比较时考虑浮点数精度问题

  • 需要分别控制实部和虚部的格式化输出时,使用 real()imag() 提取后分别打印


4. 格式化输出(fmt.Printf)

4.1 规则陈述

fmt.Printf 使用格式字符串控制输出格式,语法为 %[标志][宽度][.精度][类型]。常见类型包括:

  • %d:十进制整数

  • %f:浮点数(默认 6 位小数)

  • %.2f:保留 2 位小数的浮点数

  • %e:科学计数法

  • %.3e:保留 3 位小数的科学计数法

  • %s:字符串

  • %t:布尔值

  • %v:默认格式

  • %T:变量类型

宽度控制:%8s 表示占 8 个字符宽度,右对齐;%-8s 表示左对齐。

4.2 代码验证

正例:

package main

import "fmt"

func main() {
    var pi = 3.1415926535
    var age = 20
    var name = "Go"

    fmt.Printf("pi = %.2f\n", pi)       // pi = 3.14
    fmt.Printf("age = %d\n", age)       // age = 20
    fmt.Printf("name = %s\n", name)     // name = Go
    fmt.Printf("pi type = %T\n", pi)    // pi type = float64
}

宽度控制:

fmt.Printf("|%8s|%8d|%8.2f|\n", "张三", 20, 89.5)
// 输出:|    张三|      20|   89.50|

fmt.Printf("|%-8s|%-8d|%-8.2f|\n", "张三", 20, 89.5)
// 输出:|张三    |20      |89.50   |

4.3 机制分析

格式字符串在编译期进行部分检查。go vet 工具可以检测格式字符串与参数类型不匹配的问题:

fmt.Printf("%d", "hello")   // go vet 警告:Printf format %d has arg "hello" of wrong type string

精度 .n 必须紧跟在类型字母之前。%f 的默认精度为 6,因此 fmt.Printf("%f", 3.14) 输出 3.140000 而非 3.14

4.4 边界情况

var z = complex(3, 4)
fmt.Printf("%v", z)        // (3+4i)
fmt.Printf("%.2f", z)      // 编译错误:cannot use complex128 as type float64

复数不能直接用 %f 格式化,需分别提取实部和虚部:

fmt.Printf("%.2f+%.2fi", real(z), imag(z))   // 3.00+4.00i

fmt.Printf("|%8s|\n", "中")    // |       中|(中文字符占 3 字节,但显示宽度为 1-2 个字符,取决于终端)

4.5 实践规范

  • 浮点数输出始终显式指定精度,避免默认 6 位小数带来的多余零

  • 表格对齐时使用统一宽度,中文字符需考虑显示宽度差异

  • 复数格式化分别处理实部和虚部

  • 使用 go vet 检查格式字符串错误

5.类型转换规则

5.1 规则陈述

Go 不允许隐式类型转换,所有不同类型间的赋值和运算必须显式转换。转换语法为 目标类型(表达式)。转换可能涉及精度损失或值截断。

5.2 代码验证

整数转换(截断):

var a int32 = 300
var b int8 = int8(a)
fmt.Println(b)   // 44(300 超出 int8 范围,截断为 300 % 256 = 44)

浮点数转整数(截断小数):

var f = 3.99
var i = int(f)
fmt.Println(i)   // 3(向零截断,非四舍五入)

不同类型运算:

var a int32 = 100
var b int64 = 200
var c = a + b        // 编译错误
var d = int64(a) + b // 正确

5.3 机制分析

显式转换的设计决策源于 Go 对类型安全的追求。C 语言的隐式转换规则复杂且容易出错,例如:

int a = 1;
long b = 2;
// a + b 的结果类型取决于平台,32 位平台为 long,64 位平台可能仍为 long

Go 通过强制显式转换,使类型转换行为在代码中明确可见,消除了平台差异带来的不确定性。

5.4 边界情况

无符号与有符号转换:

var a uint8 = 255
var b int8 = int8(a)
fmt.Println(b)   // -1(255 的补码表示在 int8 中解释为 -1)

浮点数转整数溢出:

var f = 1e20
var i = int(f)    // 结果取决于平台,可能为最大值或不确定值

5.5 实践规范

  • 转换前检查值是否在目标类型范围内

  • 浮点数转整数时注意截断行为(向零截断)

  • 避免无符号与有符号类型的隐式混用


基本数据类型转为string

方式1:fmt.Sprintf("%参数",表达式)  (!!!重点)func Sprint

func Sprint(a ...interface{}) string

Sprint采用默认格式将其参数格式化,串联所有输出生成并返回一个字符串。如果两个相邻的参数都不是字符串,会在它们的输出之间添加空格。

package main
import "fmt"

func main(){
        var n1 int = 19
        var n2 float32 = 4.78
        var n3 bool = false
        var n4 byte = 'a'

        var s1 string = fmt.Sprintf("%d",n1)
        fmt.Printf("s1对应的类型是:%T ,s1 = %q \n",s1, s1)

        var s2 string = fmt.Sprintf("%f",n2)
        fmt.Printf("s2对应的类型是:%T ,s2 = %q \n",s2, s2)

        var s3 string = fmt.Sprintf("%t",n3)
        fmt.Printf("s3对应的类型是:%T ,s3 = %q \n",s3, s3)

        var s4 string = fmt.Sprintf("%c",n4)
        fmt.Printf("s4对应的类型是:%T ,s4 = %q \n",s4, s4)
}

对应结果:

s1对应的类型是:string ,s1 = "19" 
s2对应的类型是:string ,s2 = "4.780000" 
s3对应的类型是:string ,s3 = "false" 
s4对应的类型是:string ,s4 = "a" 

方式2:使用strconv包的函数  
func ParseBool

func ParseBool(str string) (value bool, err error)

返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。

func ParseInt

func ParseInt(s string, base int, bitSize int) (i int64, err error)

返回字符串表示的整数值,接受正负号。

base指定进制(2到36),如果base为0,则会从字符串前置判断,"0x"是16进制,"0"是8进制,否则是10进制;

bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。

func ParseUint

func ParseUint(s string, base int, bitSize int) (n uint64, err error)

ParseUint类似ParseInt但不接受正负号,用于无符号整型。

func ParseFloat

func ParseFloat(s string, bitSize int) (f float64, err error)

解析一个表示浮点数的字符串并返回其值。

如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;返回值err是*NumErr类型的,语法有误的,err.Error=ErrSyntax;结果超出表示范围的,返回值f为±Inf,err.Error= ErrRange。

package main
import(
        "fmt"
        "strconv"
)

func main(){
        var n1 int = 18
        var s1 string = strconv.FormatInt(int64(n1),10)  //参数:第一个参数必须转为int64类型 ,第二个参数指定字面值的进制形式为十进制
        fmt.Printf("s1对应的类型是:%T ,s1 = %q \n",s1, s1)

        var n2 float64 = 4.29
        var s2 string = strconv.FormatFloat(n2,'f',9,64)
        //第二个参数:'f'(-ddd.dddd)  第三个参数:9 保留小数点后面9位  第四个参数:表示这个小数是float64类型
        fmt.Printf("s2对应的类型是:%T ,s2 = %q \n",s2, s2)

        var n3 bool = true
        var s3 string = strconv.FormatBool(n3)
        fmt.Printf("s3对应的类型是:%T ,s3 = %q \n",s3, s3)

}

输出结果:

n1的类型bool,n1=true
n2的类型int64,n2=64
n2的类型float64,n3=3.14159
n4的类型bool,n4=false

函数注意细节:

1.Golang中函数不支持重载

2.Golang中支持可变参数 (如果你希望函数带有可变数量的参数)

package main
import "fmt"

//定义一个函数,函数的参数为:可变参数 ...  参数的数量可变
//args...int 可以传入任意多个数量的int类型的数据  传入0个,1个,,,,n个
func test (args...int){
        //函数内部处理可变参数的时候,将可变参数当做切片来处理
        //遍历可变参数:
        for i := 0; i < len(args); i++ {
                fmt.Println(args[i])
        }
}

func main(){	
        test()
        fmt.Println("--------------------")
        test(3)
        fmt.Println("--------------------")
        test(37,58,39,59,47)
}

3.在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。

package main
import "fmt"

//定义一个函数:
func test(num int){
        fmt.Println(num)
}

func main(){
        //函数也是一种数据类型,可以赋值给一个变量	
        a := test//变量就是一个函数类型的变量
        fmt.Printf("a的类型是:%T,test函数的类型是:%T \n",a,test)//a的类型是:func(int),test函数的类型是:func(int)

        //通过该变量可以对函数调用
        a(10) //等价于  test(10)
}

4.函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用

package main
import "fmt"

//定义一个函数:
func test(num int){
        fmt.Println(num)
}

//定义一个函数,把另一个函数作为形参:
func test02 (num1 int ,num2 float32, testFunc func(int)){
        fmt.Println("-----test02")
}

func main(){
        //函数也是一种数据类型,可以赋值给一个变量	
        a := test//变量就是一个函数类型的变量
        fmt.Printf("a的类型是:%T,test函数的类型是:%T \n",a,test)//a的类型是:func(int),test函数的类型是:func(int)

        //通过该变量可以对函数调用
        a(10) //等价于  test(10)

        //调用test02函数:
        test02(10,3.19,test)
        test02(10,3.19,a)
}

5.为了简化数据类型定义,Go支持自定义数据类型

基本语法: type 自定义数据类型名 数据类型

可以理解为 : 相当于起了一个别名

例如:type mylnt int ----->这时mylnt就等价int来使用了.

闭包

package main
import "fmt"

//函数功能:求和
//函数的名字:getSum 参数为空
//getSum函数返回值为一个函数,这个函数的参数是一个int类型的参数,返回值也是int类型
func getSum() func (int) int {
        var sum int = 0
        return func (num int) int{
                sum = sum + num 
                return sum
        }
}
//闭包:返回的匿名函数+匿名函数以外的变量num

func main(){
        f := getSum()
        fmt.Println(f(1))//1 
        fmt.Println(f(2))//3
        fmt.Println(f(3))//6
        fmt.Println(f(4))//10
}

匿名函数中引用的那个变量会一直保存在内存中,可以一直使用

闭包的本质:

闭包本质依旧是一个匿名函数,只是这个函数引入外界的变量/参数

匿名函数+引用的变量/参数 = 闭包

闭包的特点:

1.返回的是一个匿名函数,但是这个匿名函数引用到函数外的变量/参数 ,因此这个匿名函数就和变量/参数形成一个整体,构成闭包。

2.闭包中使用的变量/参数会一直保存在内存中,所以会一直使用---》意味着闭包不可滥用(对内存消耗大)

package main
import "fmt"

//函数功能:求和
//函数的名字:getSum 参数为空
//getSum函数返回值为一个函数,这个函数的参数是一个int类型的参数,返回值也是int类型
func getSum() func (int) int {
        var sum int = 0
        return func (num int) int{
                sum = sum + num 
                return sum
        }
}
//闭包:返回的匿名函数+匿名函数以外的变量num

func main(){
        f := getSum()
        fmt.Println(f(1))//1 
        fmt.Println(f(2))//3
        fmt.Println(f(3))//6
        fmt.Println(f(4))//10

        fmt.Println("----------------------")
        fmt.Println(getSum01(0,1))//1
        fmt.Println(getSum01(1,2))//3
        fmt.Println(getSum01(3,3))//6
        fmt.Println(getSum01(6,4))//10
}

func getSum01(sum int,num int) int{
        sum = sum + num
        return sum
}

//不使用闭包的时候:我想保留的值,不可以反复使用
//闭包应用场景:闭包可以保留上次引用的某个值,我们传入一次就可以反复使用了

defer关键字

1.defer关键字的作用:

在函数中,程序员经常需要创建资源,为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer关键字

package main

import "fmt"

func main() {
	defer fmt.Println("MyLove1")
	defer fmt.Println("MyLove2")
	fmt.Println("All MyLove")
	fmt.Println("All MyLove")
}

结果:

All MyLove2
All MyLove1
MyLove
MyLove

在Golang中,程序不会立即执行defer关键字,不会立即执行defer后面的语句,而是会将defer后面的语句压进栈里面,等函数执行到最后再从栈中取出执行


defer应用场景:

比如你想关闭某个使用的资源,在使用的时候直接随手defer,因为defer有延迟执行机制(函数执行完毕再执行defer压入栈的语句),所以你用完随手写了关闭,比较省心,省事



init函数

init函数:初始化函数,可以用来进行一些初始化的操作

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用。
全局变量定义,init函数,main函数的执行流程?

package main

import "fmt"

var num int = test()

func test() int {
	fmt.Println("test函数被执行了")
	return 3
}

func init() {
	fmt.Println("init函数被执行了")
}

func main() {
	fmt.Println("main函数被执行了")
}

结果:

test函数被执行了
init函数被执行了
main函数被执行了

如果其他包的有init函数(多个源文件有init怎么执行呢),程序如何执行呢?

package main

import (
	"fmt"
	"initfunctest/other1"
	"initfunctest/other2"
)

var num int = test0()

func test0() int {
	fmt.Println("test0函数被执行了")
	return 3
}

func init() {
	fmt.Println("main.go中init函数被执行了")
}

func main() {
	fmt.Println("main函数被执行了")
	other1.Test1()
	other2.Test2()
}

结果:

other1.go的init函数被执行了
other2.go的init函数被执行了
test0函数被执行了
main.go中init函数被执行了
main函数被执行了
other1.go的Test1函数被执行了
other2.go的Test2函数被执行了


匿名函数

Go支持匿名函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数

匿名函数使用方式:

1.在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次(用的多)

package main

import "fmt"

func main() {
	result := func(num1 int, num2 int) int {
		return num1 + num2
	}(20, 30)

	fmt.Println(result)
}

2.将匿名函数赋给一个变量(该变量就是函数变量了),再通过该变量来调用匿名函数(用的少)

package main

import "fmt"

func main() {
	plus := func(num1 int, num2 int) int {
		return num1 + num2
	}

	result1 := plus(30, 40)
	fmt.Println(result1)
	result2 := plus(20, 0)
	fmt.Println(result2)
}

结果:

70
20

3.如何让一个匿名函数,可以在整个程序中有效呢?将匿名函数给一个全局变量就可以了

package main
import "fmt"

var Func01 = func (num1 int,num2 int) int{
        return num1 * num2
}

func main(){
        //定义匿名函数:定义的同时调用
        result := func (num1 int,num2 int) int{
                return num1 + num2
        }(10,20)

        fmt.Println(result)


        //将匿名函数赋给一个变量,这个变量实际就是函数类型的变量
        //sub等价于匿名函数
        sub := func (num1 int,num2 int) int{
                return num1 - num2
        }

        //直接调用sub就是调用这个匿名函数了
        result01 := sub(30,70)
        fmt.Println(result01)

        result02 := sub(30,70)
        fmt.Println(result02)

        result03 := Func01(3,4)
        fmt.Println(result03)
}

字符串操作(忘了直接查)

1.统计字符串的长度,按字节进行统计:len(str)

使用内置函数也不用导包的,直接用就行

package main

import "fmt"

func main() {

	var str string = "Hello Golang!"
	fmt.Println(len(str))
}

结果:

13

2.字符串遍历:
利用方式1:for-range键值循环:

package main

import "fmt"

func main() {
	var str1 string = "Hello Golang!"
	for i, value := range str1 {
		fmt.Printf("索引为%d,键值为:%c\n", i, value)
	}
}
索引为0,键值为:H
索引为1,键值为:e
索引为2,键值为:l
索引为3,键值为:l
索引为4,键值为:o

利用方式2切片 :r:=[]rune(str)

package main

import "fmt"

func main() {
	var str1 string = "Hello Golang!"
	r := []rune(str1)
	for i := 0; i < len(str1); i++ {
		fmt.Printf("索引为%d,键值为:%c\n", i, r[i])
	}
}

3. 字符串转整数strconv.Atoi

Atoi 将字符串解析为 int 类型,如果字符串不是合法的整数格式,会返回错误。需要手动处理错误,避免程序崩溃。

package main

import (

	"fmt"

	"strconv"

)

func main() {

	// 正常转换

	num1, err1 := strconv.Atoi("66")

	if err1 != nil {

		fmt.Println("转换失败:", err1)

	} else {

		fmt.Printf("转换成功,num1 = %d,类型为 %T\n", num1, num1)

	}

	// 非法字符串(包含字母)

	num2, err2 := strconv.Atoi("66a")

	if err2 != nil {

		fmt.Printf("转换失败,错误信息: %v\n", err2)

	} else {

		fmt.Println(num2)

	}

	// 空字符串

	num3, err3 := strconv.Atoi("")

	if err3 != nil {

		fmt.Printf("空字符串转换失败: %v\n", err3)

	}

}

结果:

转换成功,num1 = 66,类型为 int

转换失败,错误信息: strconv.Atoi: parsing "66a": invalid syntax

空字符串转换失败: strconv.Atoi: parsing "": invalid syntax

4. 整数转字符串strconv.Itoa

Itoa 是 strconv.FormatInt(i, 10) 的简写,将 int 类型转换为十进制字符串表示,不会失败,所以不返回错误。

package main

import (

"fmt"

"strconv"

)

func main() {

var num1 int = 6887

str1 := strconv.Itoa(num1)

fmt.Printf("str1 = %q, 类型 %T\n", str1, str1)

// 负整数

num2 := -999

str2 := strconv.Itoa(num2)

fmt.Printf("str2 = %q\n", str2)

// 零值

str3 := strconv.Itoa(0)

fmt.Printf("str3 = %q\n", str3)

}

运行结果:

str1 = "6887", 类型 string

str2 = "-999"

str3 = "0"

5. 查找子串是否在指定字符串中strings.Contains

Contains 判断 substr 是否出现在 s 中,返回 bool,区分大小写。

package main

import (

"fmt"

"strings"

)

func main() {

s := "javaandgolang"

fmt.Println(strings.Contains(s, "go")) // true,因为 "go" 出现在 "golang" 中

fmt.Println(strings.Contains(s, "Go")) // false,区分大小写

fmt.Println(strings.Contains(s, "and")) // true

fmt.Println(strings.Contains(s, "python")) // false

}

运行结果:

true

false

true

false

6. 统计一个字符串有几个指定的子串strings.Count

Count 统计非重叠子串的出现次数。如果 substr 为空字符串,则返回字符串长度 + 1(Go 的特殊规则)。

package main

import (

"fmt"

"strings"

)

func main() {

s := "javaandgolang"

fmt.Println(strings.Count(s, "a")) // 4 (位置: 1,3,7,11)

fmt.Println(strings.Count(s, "an")) // 2 ("an" 出现在 "javaan" 和 "golang" 中?实际是 "javaandgolang" -> "an" 出现在 "java"+"an" 和 "golan"+"g" ?我们仔细看:s = j a v a a n d g o l a n g, "an" 出现两次:索引3-4? 注意 "java" 后是 "and",其中 "an" 一次;末尾 "lang" 中 "an" 一次,共2)

fmt.Println(strings.Count(s, "go")) // 1 ("go" 在 "golang" 开头)

fmt.Println(strings.Count(s, "x")) // 0

}

运行结果:

4

2

1

0

7. 不区分大小写的字符串比较strings.EqualFold

EqualFold 判断两个字符串是否相等,忽略大小写差异,返回 bool。它比 ToLower 后比较更高效且支持 Unicode 大小写折叠。

package main

import (

"fmt"

"strings"

)

func main() {

fmt.Println(strings.EqualFold("go", "Go")) // true

fmt.Println(strings.EqualFold("GO", "go")) // true

fmt.Println(strings.EqualFold("Golang", "gOLANg")) // true (忽略大小写)

fmt.Println(strings.EqualFold("Go", "Gopher")) // false

}

运行结果:

true
true
true
false

### 6. 返回子串在字符串第一次出现的索引值strings.Index

Index 返回 substr 在 s 中首次出现的字节索引(从 0 开始),如果未找到则返回 -1。区分大小写。

package main

import (

"fmt"

"strings"

)

func main() {

s := "javaandgolang"

fmt.Println(strings.Index(s, "a")) // 1 (第一个 'a' 在索引 1)

fmt.Println(strings.Index(s, "an")) // 3? 我们检查:s = j a v a a n d... 索引0=j,1=a,2=v,3=a,4=a,5=n, 所以 "an" 首次出现在索引 4? 实际上 "an" 出现在索引4? 让我们写清楚:s[0]='j',1='a',2='v',3='a',4='a',5='n',6='d'... "an" 在索引4开始(4='a',5='n'),所以返回 4。我们来验证代码实际输出)

fmt.Println(strings.Index(s, "go")) // 7? 实际上 "golang" 从索引7开始?s = j a v a a n d g o l a n g,索引7='g',8='o',所以返回 7

fmt.Println(strings.Index(s, "python")) // -1

}

运行结果:

1
4
7
-1