前端开发转 Go 全栈(二):从 Hello Go 到类型转换,我开始习惯 Go 的“较真”

2 阅读24分钟

前端开发转 Go 全栈(二):从 Hello Go 到类型转换,我开始习惯 Go 的“较真”

这是「前端开发转 Go 全栈」系列的第二篇。

上一篇:前端开发转 Go 全栈(一):只会前端,未来真的够用吗?

上一篇完成了 Go 开发环境的安装与验证。这一篇将正式创建第一个 Go 项目,并从变量声明、作用域一路学习到类型转换。

前言:环境装好了,终于可以开始写代码了

在上一篇文章中,我主要完成了这些内容:

为什么从前端转向全栈
→ 为什么选择 Go
→ 安装 Go
→ 验证 Go 版本
→ 认识 GOROOT 和 GOPATH
→ 确定后续学习路线

当时运行:

go version

终端成功输出:

go version go1.26.5 windows/amd64

到这里,Go 算是正式装上了。

但严格来说,上一篇还没有真正开始写 Go。

没有创建项目,没有编写 main.go,更没有体验过 Go 编译器的“脾气”。

所以这一篇,我终于可以把环境配置放到一边,正式敲下第一行 Go 代码。

原本以为入门阶段应该会比较轻松。

结果从变量声明开始,Go 编译器就不断提醒我:

这个变量没有使用
那个包没有使用
类型不一样不能计算
这里不能使用 :=

作为一名长期使用 JavaScript 和 TypeScript 的前端开发者,我逐渐发现:

Go 的语法不一定难,但它真的很喜欢把规矩提前讲清楚。

这篇文章就来记录一下,从第一个 Hello, Go! 到类型转换,我到底被 Go 编译器提醒了多少次。


创建第一个 Go 项目

我的 Go 学习目录是:

D:\LearningGO

这一次,我在里面创建第一个项目。

由于我使用的是 Git Bash,所以执行:

cd /d/LearningGO
mkdir 01-hello-go
cd 01-hello-go

然后初始化 Go Module:

go mod init hello-go

执行成功后,终端输出:

go: creating new go.mod: module hello-go

项目中也生成了一个新文件:

go.mod

当前目录结构是:

01-hello-go/
└── go.mod

go.mod 是什么?

现阶段可以先把 go.mod 理解为当前 Go 项目的模块配置文件。

打开它,可以看到类似下面的内容:

module hello-go

go 1.26

其中:

module hello-go

声明了当前模块的路径。

作为一名前端开发,可以暂时做一个不完全准确的类比:

go.mod          ≈ package.json
module hello-go ≈ package.json 中的 name

之所以说这个类比不完全准确,是因为 Go Module 不只是记录一个项目名称。

后续项目中导入自己编写的包、管理第三方依赖,都会和这里声明的模块路径有关。

在正式项目中,模块路径通常会写成代码仓库地址,例如:

github.com/username/project-name

不过目前只是本地练习,所以先使用:

hello-go

现阶段先记住一件事:

创建一个新的 Go 项目时,通常需要先执行 go mod init,初始化当前模块。


写下第一个 Hello Go

接着,我在项目中创建 main.go

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
}

现在的项目结构变成了:

01-hello-go/
├── go.mod
└── main.go

然后执行:

go run .

终端输出:

Hello, Go!

第一段 Go 代码就这样成功运行了。

虽然只是输出了一句话,但这几行代码里已经出现了几个对前端开发者来说比较陌生的内容:

package main
import "fmt"
func main()

先逐个认识一下。


package main:声明当前文件属于哪个包

第一行代码是:

package main

Go 中的每一个代码文件都必须属于某个包。

main 是一个比较特殊的包名。一个可执行程序通常需要使用 package main,并且包含一个不接收参数、没有返回值的 main 函数。

目前可以先简单理解为:

package main
+
func main()
=
这是一个可以直接运行的 Go 程序

后面学习代码拆分时,还会创建普通包。到时候再进一步理解 Go 如何通过包来组织代码。


import "fmt":导入标准库

第二部分是:

import "fmt"

这行代码导入了 Go 标准库中的 fmt 包。

fmt 主要用来处理格式化输入和输出。

例如:

fmt.Println("Hello, Go!")

从前端视角看,它和下面这段代码的作用比较接近:

console.log("Hello, Go!");

它们当然不是完全相同的东西,只是都可以用来将内容输出到终端。

这里的:

fmt.Println

可以暂时拆开理解:

fmt      导入的包
Println  fmt 包提供的函数

func main():程序从这里开始执行

接下来是:

func main() {
}

func 用来声明函数。

main 是函数名称,后面的 () 用来定义参数列表,{} 中则是函数体。

在 main 包中,main() 是程序的入口。当我们运行这个 Go 程序时,代码会从这里开始执行。

为了让第一段代码更有一点“开工仪式感”,我又增加了一行:

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
	fmt.Println("我是一名前端开发,正在学习 Go 全栈开发。")
}

再次运行:

go run .

输出:

Hello, Go!
我是一名前端开发,正在学习 Go 全栈开发。

到这里,第一个 Go 程序算是真正跑起来了。


go run 和 go build 有什么区别?

代码运行成功后,我继续执行:

go build

终端没有显示任何成功提示。

刚开始看到一片安静时,我甚至会怀疑:

它到底构建了没有?

查看项目目录后,发现多了一个文件:

hello-go.exe

目录变成了:

01-hello-go/
├── go.mod
├── main.go
└── hello-go.exe

原来 go build 成功时,通常不会专门输出一条“构建成功”。

没有错误信息,基本就代表构建已经完成。

接着运行生成的可执行文件:

./hello-go.exe

同样可以看到:

Hello, Go!
我是一名前端开发,正在学习 Go 全栈开发。

目前可以这样理解这两条命令。

go run .

读取当前目录中的代码
→ 编译
→ 立即运行

适合在开发和学习过程中快速查看运行结果。

go build

读取当前目录中的代码
→ 编译
→ 生成可执行文件

适合得到一个可以直接运行的程序。

还有一个容易忽略的细节:

修改 main.go 后,原来的 hello-go.exe 不会自动更新。

如果想让可执行文件包含最新代码,需要重新执行:

go build

这和前端开发中的热更新体验很不一样。

Go 编译出来的 .exe 是某一次构建的结果,源代码发生变化,并不会自动同步到之前生成的可执行文件中。


正式开始学习变量

完成第一个项目后,我创建了第二个练习目录:

D:\LearningGO\02-variables

进入该目录并初始化模块:

cd /d/LearningGO
mkdir 02-variables
cd 02-variables
go mod init variables

Go 中常见的变量声明方式有几种。


使用 var 完整声明变量

第一种是同时写出变量名称、类型和值:

var name string = "他们叫我秃子"
var age int = 30
var isFrontendDeveloper bool = true

它的结构是:

var 变量名 类型 = 值

和 TypeScript 的写法比较接近:

let name: string = "他们叫我秃子";
let age: number = 30;
let isFrontendDeveloper: boolean = true;

不同之处是,TypeScript 把类型写在变量名后面,并通过冒号分隔:

let age: number = 30;

Go 则直接把类型写在变量名后面:

var age int = 30

让 Go 自动推断类型

当变量类型能够根据右侧的值推断时,也可以不写出类型:

var name = "他们叫我秃子"
var age = 30

Go 会根据右侧的值推断类型:

name → string
age  → int

虽然没有明确写出类型,但变量依然拥有确定的类型。

这并不意味着它变成了 JavaScript 中的动态类型变量。

例如:

var age = 30

Go 会将 age 推断为 int。后面依然不能给它赋一个字符串:

age = "30"

使用 := 声明变量

在函数内部,还可以使用短变量声明:

job := "前端开发"
learning := "Go 全栈开发"

这种写法很短,也是 Go 代码中非常常见的变量声明方式。

例如:

name := "Starry"

可以先理解成:

声明一个名为 name 的新变量
→ 根据右侧的值推断类型
→ 将 "Starry" 赋给它

不过需要注意:

:= 只能在函数内部使用。

下面这种代码是允许的:

package main

func main() {
	name := "Starry"
}

但是不能在函数外部这样写:

package main

projectName := "前端转 Go 全栈学习"

func main() {
}

包级变量需要使用 var

package main

var projectName = "前端转 Go 全栈学习"

func main() {
}

:= 和 =:看起来很像,作用却不一样

这是我刚开始学习变量时,最容易混淆的地方。

先声明两个变量:

name := "他们叫我秃子"
age := 30

后面修改它们时,要写:

name = "Starry"
age = 31

完整代码如下:

package main

import "fmt"

func main() {
	name := "他们叫我秃子"
	age := 30

	fmt.Println("修改前姓名:", name)
	fmt.Println("修改前年龄:", age)

	name = "Starry"
	age = 31

	fmt.Println("修改后姓名:", name)
	fmt.Println("修改后年龄:", age)
}

运行结果:

修改前姓名: 他们叫我秃子
修改前年龄: 30
修改后姓名: Starry
修改后年龄: 31

这里的区别是:

:=  声明新变量并赋值
=   给已经存在的变量重新赋值

如果对同一个变量重复使用 :=

name := "Starry"
name := "陈洪董"

Go 会直接报错:

no new variables on left side of :=

翻译过来就是:

:= 左边没有任何新变量。

第二行只是想修改已经存在的 name,正确写法应该是:

name := "Starry"
name = "陈洪董"

:= 左边只要有新变量,就可以继续使用

下面这种情况是允许的:

name := "Starry"
name, age := "陈洪董", 30

因为第二行中:

name:已经存在,对它重新赋值
age:第一次出现,声明一个新变量

只要 := 左边至少存在一个当前作用域中的新变量,就可以使用短变量声明。

第一次看到这个规则时,我确实会觉得有点绕。

但结合报错再看,就比较容易理解:

:= 不是普通赋值符号,它还承担了声明变量的职责。


变量类型一旦确定,就不能随便改变

我声明了一个整数:

age := 30

然后故意给它赋一个字符串:

age = "31"

Go 编译器提示:

cannot use "31" (untyped string constant) as int value in assignment

age 在声明时已经被推断为 int 类型。

所以后续只能给它赋兼容的整数值,不能突然改成字符串。

JavaScript 中可以这样写:

let age = 30;

age = "31";

但是 Go 不允许这样做。

这也是 Go 和 JavaScript 一个非常明显的区别:

JavaScript:同一个变量可以在运行过程中保存不同类型的数据
Go:变量类型确定后,不能随意改变

这类问题会在编译阶段被直接拦下来。

不会等代码运行以后,再突然发现某个原本应该是数字的值变成了字符串。


常量声明后不能修改

Go 使用 const 声明常量:

const language = "Go"

如果尝试重新赋值:

language = "Java"

编译器会提示:

cannot assign to language

因为常量一旦声明,就不能重新赋值。

这和 JavaScript 中的 const 有一定相似之处:

const language = "Go";

不过 Go 的常量还有自己的规则和特性,目前先记住最基础的一点:

var:变量,可以重新赋值
const:常量,不能重新赋值

使用 %T 查看变量类型

为了确认 Go 到底把变量推断成了什么类型,我使用了 fmt.Printf 和 %T

package main

import "fmt"

func main() {
	name := "Starry"
	age := 30
	progress := 10.5
	isLearning := true

	fmt.Printf("name 的值:%s,类型:%T\n", name, name)
	fmt.Printf("age 的值:%d,类型:%T\n", age, age)
	fmt.Printf("progress 的值:%.1f,类型:%T\n", progress, progress)
	fmt.Printf("isLearning 的值:%t,类型:%T\n", isLearning, isLearning)
}

运行结果:

name 的值:Starry,类型:string
age 的值:30,类型:int
progress 的值:10.5,类型:float64
isLearning 的值:true,类型:bool

目前使用到的格式化符号包括:

表格

格式化符号作用
%s输出字符串
%d输出十进制整数
%f输出浮点数
%.1f保留一位小数输出浮点数
%t输出布尔值
%T输出变量类型
%v使用默认格式输出值
%q使用带引号的格式输出字符串
\n换行

这次输出也让我看到了 Go 中几个最基础的数据类型:

string
int
float64
bool

Go 的变量不会是 undefined,而是拥有零值

在 JavaScript 中,声明变量但不赋值:

let name;

console.log(name); // undefined

Go 不太一样。

如果使用 var 声明变量但不赋值:

var name string
var age int
var progress float64
var isLearning bool

这些变量会自动获得对应类型的零值。

实验代码如下:

package main

import "fmt"

func main() {
	var name string
	var age int
	var progress float64
	var isLearning bool

	fmt.Printf("name 的值:%q,类型:%T\n", name, name)
	fmt.Printf("age 的值:%d,类型:%T\n", age, age)
	fmt.Printf("progress 的值:%.1f,类型:%T\n", progress, progress)
	fmt.Printf("isLearning 的值:%t,类型:%T\n", isLearning, isLearning)
}

输出:

name 的值:"",类型:string
age 的值:0,类型:int
progress 的值:0.0,类型:float64
isLearning 的值:false,类型:bool

常见基础类型的零值如下:

表格

类型零值
string""
int0
float640
boolfalse

这意味着:

var age int

并不是一个完全没有值的变量。

它从声明开始,就已经拥有一个确定的值:

0

这个特性后面还会反复遇到。

例如,字符串转换为整数失败时,返回的整数结果可能是 0。但这个 0 只是 int 的零值,不能单独用来判断转换是否成功。


Go 不允许局部变量和导入包闲着

我尝试在函数中声明一个变量,却不使用它:

package main

func main() {
	name := "Starry"
}

运行后直接报错:

declared and not used: name

接着,我导入了 fmt,但是没有调用其中的任何内容:

package main

import "fmt"

func main() {
	name := "Starry"
	_ = name
}

编译器继续提示:

"fmt" imported and not used

这里的:

_ = name

暂时可以理解为将 name 交给空白标识符,从而明确忽略它。

在前端项目中,未使用变量和未使用导入通常由 ESLint 或 TypeScript 检查。

根据项目配置不同,它们可能是警告,也可能是错误。

Go 则更加直接:

局部变量和导入包没有使用,代码就不能通过编译。

刚开始写练习代码时,这确实有点烦。

经常只是暂时写了一个变量,准备过一会儿再使用,Go 就已经开始报错。

但从代码维护角度看,这也能避免大量没有意义的变量和依赖长期留在项目中。


批量声明变量

变量比较多时,可以集中声明:

var (
	name       = "Starry"
	age        = 30
	isFrontend = true
)

也可以在函数中同时声明多个变量:

city, language := "Singapore", "Go"

它在效果上类似于:

city := "Singapore"
language := "Go"

除此之外,也可以使用 var 同时声明多个变量:

var name, job string

因为没有赋值,所以它们都会获得 string 的零值:

""

变量的作用域

学习完变量声明后,我开始测试变量的作用范围。

目前主要遇到了三种情况:

包级作用域
函数作用域
块级作用域

包级作用域

写在函数外部的变量属于包级变量:

package main

var projectName = "前端转 Go 全栈学习"

func main() {
}

同一个包中的函数都可以访问它。

需要注意的是,包级变量不能使用 :=,必须通过 var 等方式声明。


函数作用域

写在函数内部的变量,只能在对应函数内部使用:

func main() {
	name := "Starry"
}

这里的 name 只能在 main 函数中访问。

如果另一个函数尝试直接访问它,就会报错。


块级作用域

写在 if 代码块中的变量,只在对应代码块中有效:

if isFrontend {
	target := "成为全栈开发"
	fmt.Println(target)
}

target 只存在于这对大括号中。

如果在外面访问:

fmt.Println(target)

编译器会提示:

undefined: target

这和 JavaScript 中使用 let 声明块级变量比较接近:

if (true) {
  let target = "成为全栈开发";
}

console.log(target); // 无法访问

一个容易忽略的问题:变量遮蔽

我又做了一个实验:

package main

import "fmt"

func main() {
	language := "JavaScript"

	fmt.Println("进入 if 前:", language)

	if true {
		language := "Go"
		fmt.Println("if 里面:", language)
	}

	fmt.Println("离开 if 后:", language)
}

输出:

进入 if 前: JavaScript
if 里面: Go
离开 if 后: JavaScript

看到结果后,就能发现:

language := "Go"

并没有修改外层的 language

它在 if 代码块中声明了一个新的同名变量。

内层变量暂时遮住了外层变量,这种情况叫作变量遮蔽。

如果想修改外层变量,应该使用:

if true {
	language = "Go"
}

完整代码如下:

package main

import "fmt"

func main() {
	language := "JavaScript"

	fmt.Println("进入 if 前:", language)

	if true {
		language = "Go"
		fmt.Println("if 里面:", language)
	}

	fmt.Println("离开 if 后:", language)
}

此时输出会变成:

进入 if 前: JavaScript
if 里面: Go
离开 if 后: Go

这个实验再次说明:

:= 可能创建一个新的变量
=  修改已经存在的变量

在实际业务代码中,如果不注意作用域,错误使用 :=,就可能出现:

代码明明赋值了
→ 但离开代码块后
→ 外层变量还是原来的值

这类问题不一定会直接报错,反而更值得警惕。


都是数字,Go 为什么不让我相加?

学完变量以后,我创建了第三个练习项目:

03-data-types

然后声明两个数字:

age := 30
progress := 12.5

使用 %T 查看类型:

fmt.Printf("age:%v,类型:%T\n", age, age)
fmt.Printf("progress:%v,类型:%T\n", progress, progress)

输出:

age:30,类型:int
progress:12.5,类型:float64

接着,我尝试直接相加:

result := age + progress

Go 编译器立刻提示:

invalid operation: age + progress (mismatched types int and float64)

作为一名前端开发者,我的第一反应是:

不都是数字吗?

JavaScript 和 TypeScript 里的整数、小数通常都属于 number

const age: number = 30;
const progress: number = 12.5;

const result = age + progress;

Go 则把它们区分成了不同类型:

30   → int
12.5 → float64

即使都是数字,不同类型的变量也不能直接运算。

必须明确完成类型转换:

result := float64(age) + progress

完整代码如下:

package main

import "fmt"

func main() {
	age := 30
	progress := 12.5

	result := float64(age) + progress

	fmt.Printf("计算结果:%v\n", result)
	fmt.Printf("结果类型:%T\n", result)
}

输出:

计算结果:42.5
结果类型:float64

这也是我逐渐感受到的 Go 风格:

Go 不喜欢在背后猜开发者的意图。

到底要把整数转换成浮点数,还是把浮点数转换成整数,需要由开发者自己明确选择。


float64 转 int,小数部分直接消失了

接着,我又尝试将 float64 转换成 int

price := 19.99
integerPrice := int(price)

查看转换前后的值和类型:

fmt.Printf("转换前:%v,类型:%T\n", price, price)
fmt.Printf("转换后:%v,类型:%T\n", integerPrice, integerPrice)

输出:

转换前:19.99,类型:float64
转换后:19,类型:int

int(19.99) 得到的不是 20,而是:

19

浮点数转换为整数时,会直接截去小数部分,不会自动四舍五入:

int(19.99) // 19
int(19.50) // 19
int(19.01) // 19

负数同样是向零截断:

int(-19.99) // -19

如果涉及价格和精度,这种转换就需要格外谨慎。

少掉的 0.99 不会报错,也不会提醒你。

它就这么安静地没了。

而且真实业务中的金额通常也不适合直接使用浮点数处理,因为浮点数本身还存在精度问题。更常见的方式是使用最小货币单位保存整数,例如:

19.99 元
→ 保存为 1999 分

具体的金额处理方式后续再单独学习。


我以为 string(65) 会得到 "65"

接下来,我开始测试数字和字符串之间的转换。

按照 JavaScript 的习惯,我很自然地写出了:

number := 65
result := string(number)

原本以为会得到:

"65"

实际结果却是:

值:"A"
类型:string

完整实验代码如下:

package main

import "fmt"

func main() {
	number := 65
	result := string(number)

	fmt.Printf("值:%q\n", result)
	fmt.Printf("类型:%T\n", result)
}

原因是:

string(65)

不是把整数格式化成十进制字符串,而是将 65 作为 Unicode 码点进行转换。

数字 65 对应的字符正好是:

A

所以:

string(65) // "A"

而 JavaScript 中:

String(65); // "65"

在 Go 里,如果真正想把整数转换成十进制数字字符串,需要使用 strconv.Itoa


使用 strconv 完成数字与字符串转换

strconv 是 Go 标准库中用于基础数据类型与字符串之间转换的包。

使用之前需要先导入:

import "strconv"

如果还需要输出结果,可以同时导入 fmt 和 strconv

import (
	"fmt"
	"strconv"
)

int 转 string

将 int 转换为对应的十进制字符串,可以使用:

strconv.Itoa()

例如:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	age := 30
	ageText := strconv.Itoa(age)

	fmt.Printf("转换前:%v,类型:%T\n", age, age)
	fmt.Printf("转换后:%v,类型:%T\n", ageText, ageText)
}

输出:

转换前:30,类型:int
转换后:30,类型:string

虽然终端显示的都是 30,但它们的实际类型已经不同:

30   → int
"30" → string

所以,下面两种写法表达的是完全不同的事情:

string(65)       // "A"
strconv.Itoa(65) // "65"

这是这一阶段最容易记错的地方之一。


string 转 int

字符串转整数可以使用:

strconv.Atoi()

例如:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	ageText := "30"

	age, err := strconv.Atoi(ageText)

	fmt.Printf("转换结果:%v\n", age)
	fmt.Printf("结果类型:%T\n", age)
	fmt.Printf("错误信息:%v\n", err)
}

输出:

转换结果:30
结果类型:int
错误信息:<nil>

这里一次得到了两个结果:

age:转换后的整数
err:转换过程中产生的错误

这次转换成功,所以 err 是:

<nil>

现阶段可以先理解为:

没有发生错误

至于为什么一个函数可以一次返回两个值,以及 error 和 nil 到底是什么,留到下一篇再正式学习。


把 "30岁" 转成 int 会发生什么?

接着,我故意传入一个不能正常转换的字符串:

ageText := "30岁"

age, err := strconv.Atoi(ageText)

完整代码如下:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	ageText := "30岁"

	age, err := strconv.Atoi(ageText)

	fmt.Printf("转换结果:%v\n", age)
	fmt.Printf("错误信息:%v\n", err)
}

输出:

转换结果:0
错误信息:strconv.Atoi: parsing "30岁": invalid syntax

这里的 0 并不代表转换成功。

它只是 int 类型的零值。

所以不能只根据转换结果判断是否失败:

if age == 0 {
	fmt.Println("转换失败")
}

因为用户真的传入 "0" 时,转换结果同样是 0

例如:

age, err := strconv.Atoi("0")

此时:

age = 0
err = nil

这实际上是一次成功的转换。

因此,应该根据 err 判断转换是否失败:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	ageText := "30岁"

	age, err := strconv.Atoi(ageText)

	if err != nil {
		fmt.Println("年龄格式不正确:", err)
		return
	}

	fmt.Println("年龄:", age)
}

运行结果:

年龄格式不正确: strconv.Atoi: parsing "30岁": invalid syntax

这一次,我只是先学会如何使用这段代码:

if err != nil {
	return
}

关于 err != nilreturn 和 Go 的错误处理方式,下一篇再单独研究。


string 转 float64

字符串转浮点数可以使用:

strconv.ParseFloat()

例如:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	priceText := "19.99"

	price, err := strconv.ParseFloat(priceText, 64)

	if err != nil {
		fmt.Println("价格格式错误:", err)
		return
	}

	fmt.Printf("价格:%v\n", price)
	fmt.Printf("类型:%T\n", price)
}

输出:

价格:19.99
类型:float64

这里的:

strconv.ParseFloat(priceText, 64)

可以暂时理解为:

priceText:需要转换的字符串
64:希望按照 float64 的精度解析

如果字符串不是有效的浮点数,例如:

priceText := "19.99元"

err 就不会是 nil


float64 转 string

反过来,float64 转换为字符串可以使用:

strconv.FormatFloat()

例如:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	price := 19.99

	priceText := strconv.FormatFloat(price, 'f', 2, 64)

	fmt.Printf("转换前:%v,类型:%T\n", price, price)
	fmt.Printf("转换后:%v,类型:%T\n", priceText, priceText)
}

输出:

转换前:19.99,类型:float64
转换后:19.99,类型:string

目前可以先这样理解它的参数:

strconv.FormatFloat(price, 'f', 2, 64)

表格

参数作用
price需要转换的浮点数
'f'使用普通小数格式
2小数点后保留两位
64原始值是 float64

例如:

price := 19.9
priceText := strconv.FormatFloat(price, 'f', 2, 64)

得到的字符串是:

"19.90"

这里需要注意,保留两位小数描述的是生成字符串时的格式,不是解决浮点数精度问题。


这一阶段用到的类型转换

目前实际使用到的类型转换可以整理成下面这张表:

表格

转换方向写法
int 转 float64float64(value)
float64 转 intint(value)
int 转 stringstrconv.Itoa(value)
string 转 intstrconv.Atoi(value)
string 转 float64strconv.ParseFloat(value, 64)
float64 转 stringstrconv.FormatFloat(value, 'f', 2, 64)

其中最容易记错的是:

string(65)       // "A"
strconv.Itoa(65) // "65"

看到 Go 中的 string(),不能直接按照 JavaScript 中 String() 的方式理解。


从前端视角看,Go 的类型转换有什么不同?

经过这一轮练习,我发现 JavaScript 和 Go 在类型转换上的思路有明显区别。

JavaScript 经常会自动完成隐式类型转换:

"10" + 5; // "105"
"10" - 5; // 5

同样是字符串和数字参与运算,结果却可能完全不同。

如果不了解 JavaScript 的类型转换规则,很容易写出一些看起来奇怪的结果。

Go 的处理方式则更加直接。

下面的代码不能通过编译:

text := "10"
number := 5

result := text + number

开发者必须先明确决定要做什么。

如果想完成字符串拼接,可以把整数转换成字符串:

result := text + strconv.Itoa(number)

结果是:

105

如果想完成数学运算,就要先把字符串转换成整数:

textNumber, err := strconv.Atoi(text)

if err != nil {
	fmt.Println("转换失败:", err)
	return
}

result := textNumber + number

结果是:

15

Go 不替开发者猜测:

你到底想拼接字符串
还是想进行数学运算?

必须由代码明确表达。


写在最后

这一篇实际完成的内容比我原本预想得更多。

从第一个程序开始,一路学习到了:

创建 Go Module
→ 编写 main.go
→ go run
→ go build
→ 变量与常量
→ := 与 =
→ 类型推断
→ 零值
→ 作用域
→ 变量遮蔽
→ int 和 float64
→ 数字与字符串转换

这一阶段让我对 Go 最明显的感受是:

Go 不喜欢替开发者猜。

变量类型确定以后,不能随意改变。

不同数字类型的变量不能直接计算。

浮点数转换成整数时,小数部分会被直接截去。

整数转换成字符串,也不能看到 string() 就直接套用 JavaScript 中 String() 的理解。

刚开始会觉得它有点“较真”:

不都是数字吗?
为什么还要手动转换?
我就是想改个变量,为什么不能继续使用 :=?
明明写了一个变量,暂时没用为什么也不行?

但这些报错也在不断提醒我:

这个变量到底是什么类型?
这里是在声明变量,还是在修改变量?
类型转换以后会不会丢失数据?
用户传入的字符串真的可以转换吗?
这次转换到底成功了,还是只得到了一个零值?

这些问题在写页面时可能不一定经常出现,但在处理后端数据时,却很难绕开。

用户从请求中传来的参数,很多时候本来就是字符串:

?page=1
&size=20
&price=19.99

如果后端想用这些数据完成分页、计算和数据库查询,就必须先完成转换,并处理可能发生的错误。

目前,我已经在 strconv.Atoi 和 strconv.ParseFloat 中多次写过:

value, err := ...

但我还没有正式弄清楚:

一个函数为什么可以返回多个值?
err 到底是什么类型?
nil 表示什么?
为什么 Go 代码里到处都是 if err != nil?
return 除了结束函数,还能返回什么?

所以下一篇,将正式进入:

函数
→ 参数与返回值
→ 多返回值
→ error
→ nil
→ 错误处理

下一篇见。