前端开发转 Go 全栈(二):从 Hello Go 到类型转换,我开始习惯 Go 的"较真"
这是「前端开发转 Go 全栈」系列的第二篇。
上一篇:前端开发转 Go 全栈(一):只会前端,未来真的够用吗?
上一篇完成了 Go 开发环境的安装与验证。这一篇将正式创建第一个 Go 项目,并从变量声明、作用域一路学习到类型转换。
前言:环境装好了,终于可以开始写代码了
在上一篇文章中,我主要完成了这些内容:
text
为什么从前端转向全栈
→ 为什么选择 Go
→ 安装 Go
→ 验证 Go 版本
→ 认识 GOROOT 和 GOPATH
→ 确定后续学习路线
当时运行:
bash
go version
终端成功输出:
text
go version go1.26.5 windows/amd64
到这里,Go 算是正式装上了。
但严格来说,上一篇还没有真正开始写 Go。
没有创建项目,没有编写 main.go,更没有体验过 Go 编译器的"脾气"。
所以这一篇,我终于可以把环境配置放到一边,正式敲下第一行 Go 代码。
原本以为入门阶段应该会比较轻松。
结果从变量声明开始,Go 编译器就不断提醒我:
text
这个变量没有使用
那个包没有使用
类型不一样不能计算
这里不能使用 :=
作为一名长期使用 JavaScript 和 TypeScript 的前端开发者,我逐渐发现:
Go 的语法不一定难,但它真的很喜欢把规矩提前讲清楚。
这篇文章就来记录一下,从第一个 Hello, Go! 到类型转换,我到底被 Go 编译器提醒了多少次。
创建第一个 Go 项目
我的 Go 学习目录是:
text
D:\LearningGO
这一次,我在里面创建第一个项目。
由于我使用的是 Git Bash,所以执行:
bash
cd /d/LearningGO
mkdir 01-hello-go
cd 01-hello-go
然后初始化 Go Module:
bash
go mod init hello-go
执行成功后,终端输出:
text
go: creating new go.mod: module hello-go
项目中也生成了一个新文件:
text
go.mod
当前目录结构是:
text
01-hello-go/
└── go.mod
go.mod 是什么?
现阶段可以先把 go.mod 理解为当前 Go 项目的模块配置文件。
打开它,可以看到类似下面的内容:
go
module hello-go
go 1.26
其中:
go
module hello-go
声明了当前模块的路径。
作为一名前端开发,可以暂时做一个不完全准确的类比:
text
go.mod ≈ package.json
module hello-go ≈ package.json 中的 name
之所以说这个类比不完全准确,是因为 Go Module 不只是记录一个项目名称。
后续项目中导入自己编写的包、管理第三方依赖,都会和这里声明的模块路径有关。
在正式项目中,模块路径通常会写成代码仓库地址,例如:
text
github.com/username/project-name
不过目前只是本地练习,所以先使用:
text
hello-go
现阶段先记住一件事:
创建一个新的 Go 项目时,通常需要先执行
go mod init,初始化当前模块。
写下第一个 Hello Go
接着,我在项目中创建 main.go:
go
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
现在的项目结构变成了:
text
01-hello-go/
├── go.mod
└── main.go
然后执行:
bash
go run .
终端输出:
text
Hello, Go!
第一段 Go 代码就这样成功运行了。
虽然只是输出了一句话,但这几行代码里已经出现了几个对前端开发者来说比较陌生的内容:
go
package main
import "fmt"
func main()
先逐个认识一下。
package main:声明当前文件属于哪个包
第一行代码是:
go
package main
Go 中的每一个代码文件都必须属于某个包。
main 是一个比较特殊的包名。一个可执行程序通常需要使用 package main,并且包含一个不接收参数、没有返回值的 main 函数。
目前可以先简单理解为:
text
package main
+
func main()
=
这是一个可以直接运行的 Go 程序
后面学习代码拆分时,还会创建普通包。到时候再进一步理解 Go 如何通过包来组织代码。
import "fmt":导入标准库
第二部分是:
go
import "fmt"
这行代码导入了 Go 标准库中的 fmt 包。
fmt 主要用来处理格式化输入和输出。
例如:
go
fmt.Println("Hello, Go!")
从前端视角看,它和下面这段代码的作用比较接近:
javascript
console.log("Hello, Go!");
它们当然不是完全相同的东西,只是都可以用来将内容输出到终端。
这里的:
go
fmt.Println
可以暂时拆开理解:
text
fmt 导入的包
Println fmt 包提供的函数
func main():程序从这里开始执行
接下来是:
go
func main() {
}
func 用来声明函数。
main 是函数名称,后面的 () 用来定义参数列表,{} 中则是函数体。
在 main 包中,main() 是程序的入口。当我们运行这个 Go 程序时,代码会从这里开始执行。
为了让第一段代码更有一点"开工仪式感",我又增加了一行:
go
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
fmt.Println("我是一名前端开发,正在学习 Go 全栈开发。")
}
再次运行:
bash
go run .
输出:
text
Hello, Go!
我是一名前端开发,正在学习 Go 全栈开发。
到这里,第一个 Go 程序算是真正跑起来了。
go run 和 go build 有什么区别?
代码运行成功后,我继续执行:
bash
go build
终端没有显示任何成功提示。
刚开始看到一片安静时,我甚至会怀疑:
它到底构建了没有?
查看项目目录后,发现多了一个文件:
text
hello-go.exe
目录变成了:
text
01-hello-go/
├── go.mod
├── main.go
└── hello-go.exe
原来 go build 成功时,通常不会专门输出一条"构建成功"。
没有错误信息,基本就代表构建已经完成。
接着运行生成的可执行文件:
bash
./hello-go.exe
同样可以看到:
text
Hello, Go!
我是一名前端开发,正在学习 Go 全栈开发。
目前可以这样理解这两条命令。
go run .
text
读取当前目录中的代码
→ 编译
→ 立即运行
适合在开发和学习过程中快速查看运行结果。
go build
text
读取当前目录中的代码
→ 编译
→ 生成可执行文件
适合得到一个可以直接运行的程序。
还有一个容易忽略的细节:
修改
main.go后,原来的hello-go.exe不会自动更新。
如果想让可执行文件包含最新代码,需要重新执行:
bash
go build
这和前端开发中的热更新体验很不一样。
Go 编译出来的 .exe 是某一次构建的结果,源代码发生变化,并不会自动同步到之前生成的可执行文件中。
正式开始学习变量
完成第一个项目后,我创建了第二个练习目录:
text
D:\LearningGO\02-variables
进入该目录并初始化模块:
bash
cd /d/LearningGO
mkdir 02-variables
cd 02-variables
go mod init variables
Go 中常见的变量声明方式有几种。
使用 var 完整声明变量
第一种是同时写出变量名称、类型和值:
go
var name string = "他们叫我秃子"
var age int = 30
var isFrontendDeveloper bool = true
它的结构是:
text
var 变量名 类型 = 值
和 TypeScript 的写法比较接近:
typescript
let name: string = "他们叫我秃子";
let age: number = 30;
let isFrontendDeveloper: boolean = true;
不同之处是,TypeScript 把类型写在变量名后面,并通过冒号分隔:
typescript
let age: number = 30;
Go 则直接把类型写在变量名后面:
go
var age int = 30
让 Go 自动推断类型
当变量类型能够根据右侧的值推断时,也可以不写出类型:
go
var name = "他们叫我秃子"
var age = 30
Go 会根据右侧的值推断类型:
text
name → string
age → int
虽然没有明确写出类型,但变量依然拥有确定的类型。
这并不意味着它变成了 JavaScript 中的动态类型变量。
例如:
go
var age = 30
Go 会将 age 推断为 int。后面依然不能给它赋一个字符串:
go
age = "30"
使用 := 声明变量
在函数内部,还可以使用短变量声明:
go
job := "前端开发"
learning := "Go 全栈开发"
这种写法很短,也是 Go 代码中非常常见的变量声明方式。
例如:
go
name := "Starry"
可以先理解成:
text
声明一个名为 name 的新变量
→ 根据右侧的值推断类型
→ 将 "Starry" 赋给它
不过需要注意:
:=只能在函数内部使用。
下面这种代码是允许的:
go
package main
func main() {
name := "Starry"
}
但是不能在函数外部这样写:
go
package main
projectName := "前端转 Go 全栈学习"
func main() {
}
包级变量需要使用 var:
go
package main
var projectName = "前端转 Go 全栈学习"
func main() {
}
:= 和 =:看起来很像,作用却不一样
这是我刚开始学习变量时,最容易混淆的地方。
先声明两个变量:
go
name := "他们叫我秃子"
age := 30
后面修改它们时,要写:
go
name = "Starry"
age = 31
完整代码如下:
go
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)
}
运行结果:
text
修改前姓名: 他们叫我秃子
修改前年龄: 30
修改后姓名: Starry
修改后年龄: 31
这里的区别是:
text
:= 声明新变量并赋值
= 给已经存在的变量重新赋值
如果对同一个变量重复使用 :=:
go
name := "Starry"
name := "陈洪董"
Go 会直接报错:
text
no new variables on left side of :=
翻译过来就是:
:=左边没有任何新变量。
第二行只是想修改已经存在的 name,正确写法应该是:
go
name := "Starry"
name = "陈洪董"
:= 左边只要有新变量,就可以继续使用
下面这种情况是允许的:
go
name := "Starry"
name, age := "陈洪董", 30
因为第二行中:
text
name:已经存在,对它重新赋值
age:第一次出现,声明一个新变量
只要 := 左边至少存在一个当前作用域中的新变量,就可以使用短变量声明。
第一次看到这个规则时,我确实会觉得有点绕。
但结合报错再看,就比较容易理解:
:=不是普通赋值符号,它还承担了声明变量的职责。
变量类型一旦确定,就不能随便改变
我声明了一个整数:
go
age := 30
然后故意给它赋一个字符串:
go
age = "31"
Go 编译器提示:
text
cannot use "31" (untyped string constant) as int value in assignment
age 在声明时已经被推断为 int 类型。
所以后续只能给它赋兼容的整数值,不能突然改成字符串。
JavaScript 中可以这样写:
javascript
let age = 30;
age = "31";
但是 Go 不允许这样做。
这也是 Go 和 JavaScript 一个非常明显的区别:
text
JavaScript:同一个变量可以在运行过程中保存不同类型的数据
Go:变量类型确定后,不能随意改变
这类问题会在编译阶段被直接拦下来。
不会等代码运行以后,再突然发现某个原本应该是数字的值变成了字符串。
常量声明后不能修改
Go 使用 const 声明常量:
go
const language = "Go"
如果尝试重新赋值:
go
language = "Java"
编译器会提示:
text
cannot assign to language
因为常量一旦声明,就不能重新赋值。
这和 JavaScript 中的 const 有一定相似之处:
javascript
const language = "Go";
不过 Go 的常量还有自己的规则和特性,目前先记住最基础的一点:
text
var:变量,可以重新赋值
const:常量,不能重新赋值
使用 %T 查看变量类型
为了确认 Go 到底把变量推断成了什么类型,我使用了 fmt.Printf 和 %T:
go
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)
}
运行结果:
text
name 的值:Starry,类型:string
age 的值:30,类型:int
progress 的值:10.5,类型:float64
isLearning 的值:true,类型:bool
目前使用到的格式化符号包括:
表格
| 格式化符号 | 作用 |
|---|---|
%s |
输出字符串 |
%d |
输出十进制整数 |
%f |
输出浮点数 |
%.1f |
保留一位小数输出浮点数 |
%t |
输出布尔值 |
%T |
输出变量类型 |
%v |
使用默认格式输出值 |
%q |
使用带引号的格式输出字符串 |
\n |
换行 |
这次输出也让我看到了 Go 中几个最基础的数据类型:
text
string
int
float64
bool
Go 的变量不会是 undefined,而是拥有零值
在 JavaScript 中,声明变量但不赋值:
javascript
let name;
console.log(name); // undefined
Go 不太一样。
如果使用 var 声明变量但不赋值:
go
var name string
var age int
var progress float64
var isLearning bool
这些变量会自动获得对应类型的零值。
实验代码如下:
go
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)
}
输出:
text
name 的值:"",类型:string
age 的值:0,类型:int
progress 的值:0.0,类型:float64
isLearning 的值:false,类型:bool
常见基础类型的零值如下:
表格
| 类型 | 零值 |
|---|---|
string |
"" |
int |
0 |
float64 |
0 |
bool |
false |
这意味着:
go
var age int
并不是一个完全没有值的变量。
它从声明开始,就已经拥有一个确定的值:
text
0
这个特性后面还会反复遇到。
例如,字符串转换为整数失败时,返回的整数结果可能是 0。但这个 0 只是 int 的零值,不能单独用来判断转换是否成功。
Go 不允许局部变量和导入包闲着
我尝试在函数中声明一个变量,却不使用它:
go
package main
func main() {
name := "Starry"
}
运行后直接报错:
text
declared and not used: name
接着,我导入了 fmt,但是没有调用其中的任何内容:
go
package main
import "fmt"
func main() {
name := "Starry"
_ = name
}
编译器继续提示:
text
"fmt" imported and not used
这里的:
go
_ = name
暂时可以理解为将 name 交给空白标识符,从而明确忽略它。
在前端项目中,未使用变量和未使用导入通常由 ESLint 或 TypeScript 检查。
根据项目配置不同,它们可能是警告,也可能是错误。
Go 则更加直接:
局部变量和导入包没有使用,代码就不能通过编译。
刚开始写练习代码时,这确实有点烦。
经常只是暂时写了一个变量,准备过一会儿再使用,Go 就已经开始报错。
但从代码维护角度看,这也能避免大量没有意义的变量和依赖长期留在项目中。
批量声明变量
变量比较多时,可以集中声明:
go
var (
name = "Starry"
age = 30
isFrontend = true
)
也可以在函数中同时声明多个变量:
go
city, language := "Singapore", "Go"
它在效果上类似于:
go
city := "Singapore"
language := "Go"
除此之外,也可以使用 var 同时声明多个变量:
go
var name, job string
因为没有赋值,所以它们都会获得 string 的零值:
text
""
变量的作用域
学习完变量声明后,我开始测试变量的作用范围。
目前主要遇到了三种情况:
text
包级作用域
函数作用域
块级作用域
包级作用域
写在函数外部的变量属于包级变量:
go
package main
var projectName = "前端转 Go 全栈学习"
func main() {
}
同一个包中的函数都可以访问它。
需要注意的是,包级变量不能使用 :=,必须通过 var 等方式声明。
函数作用域
写在函数内部的变量,只能在对应函数内部使用:
go
func main() {
name := "Starry"
}
这里的 name 只能在 main 函数中访问。
如果另一个函数尝试直接访问它,就会报错。
块级作用域
写在 if 代码块中的变量,只在对应代码块中有效:
go
if isFrontend {
target := "成为全栈开发"
fmt.Println(target)
}
target 只存在于这对大括号中。
如果在外面访问:
go
fmt.Println(target)
编译器会提示:
text
undefined: target
这和 JavaScript 中使用 let 声明块级变量比较接近:
javascript
if (true) {
let target = "成为全栈开发";
}
console.log(target); // 无法访问
一个容易忽略的问题:变量遮蔽
我又做了一个实验:
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)
}
输出:
text
进入 if 前: JavaScript
if 里面: Go
离开 if 后: JavaScript
看到结果后,就能发现:
go
language := "Go"
并没有修改外层的 language。
它在 if 代码块中声明了一个新的同名变量。
内层变量暂时遮住了外层变量,这种情况叫作变量遮蔽。
如果想修改外层变量,应该使用:
go
if true {
language = "Go"
}
完整代码如下:
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)
}
此时输出会变成:
text
进入 if 前: JavaScript
if 里面: Go
离开 if 后: Go
这个实验再次说明:
text
:= 可能创建一个新的变量
= 修改已经存在的变量
在实际业务代码中,如果不注意作用域,错误使用 :=,就可能出现:
text
代码明明赋值了
→ 但离开代码块后
→ 外层变量还是原来的值
这类问题不一定会直接报错,反而更值得警惕。
都是数字,Go 为什么不让我相加?
学完变量以后,我创建了第三个练习项目:
text
03-data-types
然后声明两个数字:
go
age := 30
progress := 12.5
使用 %T 查看类型:
go
fmt.Printf("age:%v,类型:%T\n", age, age)
fmt.Printf("progress:%v,类型:%T\n", progress, progress)
输出:
text
age:30,类型:int
progress:12.5,类型:float64
接着,我尝试直接相加:
go
result := age + progress
Go 编译器立刻提示:
text
invalid operation: age + progress (mismatched types int and float64)
作为一名前端开发者,我的第一反应是:
不都是数字吗?
JavaScript 和 TypeScript 里的整数、小数通常都属于 number:
typescript
const age: number = 30;
const progress: number = 12.5;
const result = age + progress;
Go 则把它们区分成了不同类型:
text
30 → int
12.5 → float64
即使都是数字,不同类型的变量也不能直接运算。
必须明确完成类型转换:
go
result := float64(age) + progress
完整代码如下:
go
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)
}
输出:
text
计算结果:42.5
结果类型:float64
这也是我逐渐感受到的 Go 风格:
Go 不喜欢在背后猜开发者的意图。
到底要把整数转换成浮点数,还是把浮点数转换成整数,需要由开发者自己明确选择。
float64 转 int,小数部分直接消失了
接着,我又尝试将 float64 转换成 int:
go
price := 19.99
integerPrice := int(price)
查看转换前后的值和类型:
go
fmt.Printf("转换前:%v,类型:%T\n", price, price)
fmt.Printf("转换后:%v,类型:%T\n", integerPrice, integerPrice)
输出:
text
转换前:19.99,类型:float64
转换后:19,类型:int
int(19.99) 得到的不是 20,而是:
text
19
浮点数转换为整数时,会直接截去小数部分,不会自动四舍五入:
go
int(19.99) // 19
int(19.50) // 19
int(19.01) // 19
负数同样是向零截断:
go
int(-19.99) // -19
如果涉及价格和精度,这种转换就需要格外谨慎。
少掉的 0.99 不会报错,也不会提醒你。
它就这么安静地没了。
而且真实业务中的金额通常也不适合直接使用浮点数处理,因为浮点数本身还存在精度问题。更常见的方式是使用最小货币单位保存整数,例如:
text
19.99 元
→ 保存为 1999 分
具体的金额处理方式后续再单独学习。
我以为 string(65) 会得到 "65"
接下来,我开始测试数字和字符串之间的转换。
按照 JavaScript 的习惯,我很自然地写出了:
go
number := 65
result := string(number)
原本以为会得到:
text
"65"
实际结果却是:
text
值:"A"
类型:string
完整实验代码如下:
go
package main
import "fmt"
func main() {
number := 65
result := string(number)
fmt.Printf("值:%q\n", result)
fmt.Printf("类型:%T\n", result)
}
原因是:
go
string(65)
不是把整数格式化成十进制字符串,而是将 65 作为 Unicode 码点进行转换。
数字 65 对应的字符正好是:
text
A
所以:
go
string(65) // "A"
而 JavaScript 中:
javascript
String(65); // "65"
在 Go 里,如果真正想把整数转换成十进制数字字符串,需要使用 strconv.Itoa。
使用 strconv 完成数字与字符串转换
strconv 是 Go 标准库中用于基础数据类型与字符串之间转换的包。
使用之前需要先导入:
go
import "strconv"
如果还需要输出结果,可以同时导入 fmt 和 strconv:
go
import (
"fmt"
"strconv"
)
int 转 string
将 int 转换为对应的十进制字符串,可以使用:
go
strconv.Itoa()
例如:
go
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)
}
输出:
text
转换前:30,类型:int
转换后:30,类型:string
虽然终端显示的都是 30,但它们的实际类型已经不同:
text
30 → int
"30" → string
所以,下面两种写法表达的是完全不同的事情:
go
string(65) // "A"
strconv.Itoa(65) // "65"
这是这一阶段最容易记错的地方之一。
string 转 int
字符串转整数可以使用:
go
strconv.Atoi()
例如:
go
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)
}
输出:
text
转换结果:30
结果类型:int
错误信息:<nil>
这里一次得到了两个结果:
text
age:转换后的整数
err:转换过程中产生的错误
这次转换成功,所以 err 是:
text
<nil>
现阶段可以先理解为:
text
没有发生错误
至于为什么一个函数可以一次返回两个值,以及 error 和 nil 到底是什么,留到下一篇再正式学习。
把 "30岁" 转成 int 会发生什么?
接着,我故意传入一个不能正常转换的字符串:
go
ageText := "30岁"
age, err := strconv.Atoi(ageText)
完整代码如下:
go
package main
import (
"fmt"
"strconv"
)
func main() {
ageText := "30岁"
age, err := strconv.Atoi(ageText)
fmt.Printf("转换结果:%v\n", age)
fmt.Printf("错误信息:%v\n", err)
}
输出:
text
转换结果:0
错误信息:strconv.Atoi: parsing "30岁": invalid syntax
这里的 0 并不代表转换成功。
它只是 int 类型的零值。
所以不能只根据转换结果判断是否失败:
go
if age == 0 {
fmt.Println("转换失败")
}
因为用户真的传入 "0" 时,转换结果同样是 0。
例如:
go
age, err := strconv.Atoi("0")
此时:
text
age = 0
err = nil
这实际上是一次成功的转换。
因此,应该根据 err 判断转换是否失败:
go
package main
import (
"fmt"
"strconv"
)
func main() {
ageText := "30岁"
age, err := strconv.Atoi(ageText)
if err != nil {
fmt.Println("年龄格式不正确:", err)
return
}
fmt.Println("年龄:", age)
}
运行结果:
text
年龄格式不正确: strconv.Atoi: parsing "30岁": invalid syntax
这一次,我只是先学会如何使用这段代码:
go
if err != nil {
return
}
关于 err != nil、return 和 Go 的错误处理方式,下一篇再单独研究。
string 转 float64
字符串转浮点数可以使用:
go
strconv.ParseFloat()
例如:
go
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)
}
输出:
text
价格:19.99
类型:float64
这里的:
go
strconv.ParseFloat(priceText, 64)
可以暂时理解为:
text
priceText:需要转换的字符串
64:希望按照 float64 的精度解析
如果字符串不是有效的浮点数,例如:
go
priceText := "19.99元"
err 就不会是 nil。
float64 转 string
反过来,float64 转换为字符串可以使用:
go
strconv.FormatFloat()
例如:
go
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)
}
输出:
text
转换前:19.99,类型:float64
转换后:19.99,类型:string
目前可以先这样理解它的参数:
go
strconv.FormatFloat(price, 'f', 2, 64)
表格
| 参数 | 作用 |
|---|---|
price |
需要转换的浮点数 |
'f' |
使用普通小数格式 |
2 |
小数点后保留两位 |
64 |
原始值是 float64 |
例如:
go
price := 19.9
priceText := strconv.FormatFloat(price, 'f', 2, 64)
得到的字符串是:
text
"19.90"
这里需要注意,保留两位小数描述的是生成字符串时的格式,不是解决浮点数精度问题。
这一阶段用到的类型转换
目前实际使用到的类型转换可以整理成下面这张表:
表格
| 转换方向 | 写法 |
|---|---|
int 转 float64 |
float64(value) |
float64 转 int |
int(value) |
int 转 string |
strconv.Itoa(value) |
string 转 int |
strconv.Atoi(value) |
string 转 float64 |
strconv.ParseFloat(value, 64) |
float64 转 string |
strconv.FormatFloat(value, 'f', 2, 64) |
其中最容易记错的是:
go
string(65) // "A"
strconv.Itoa(65) // "65"
看到 Go 中的 string(),不能直接按照 JavaScript 中 String() 的方式理解。
从前端视角看,Go 的类型转换有什么不同?
经过这一轮练习,我发现 JavaScript 和 Go 在类型转换上的思路有明显区别。
JavaScript 经常会自动完成隐式类型转换:
javascript
"10" + 5; // "105"
"10" - 5; // 5
同样是字符串和数字参与运算,结果却可能完全不同。
如果不了解 JavaScript 的类型转换规则,很容易写出一些看起来奇怪的结果。
Go 的处理方式则更加直接。
下面的代码不能通过编译:
go
text := "10"
number := 5
result := text + number
开发者必须先明确决定要做什么。
如果想完成字符串拼接,可以把整数转换成字符串:
go
result := text + strconv.Itoa(number)
结果是:
text
105
如果想完成数学运算,就要先把字符串转换成整数:
go
textNumber, err := strconv.Atoi(text)
if err != nil {
fmt.Println("转换失败:", err)
return
}
result := textNumber + number
结果是:
text
15
Go 不替开发者猜测:
text
你到底想拼接字符串
还是想进行数学运算?
必须由代码明确表达。
写在最后
这一篇实际完成的内容比我原本预想得更多。
从第一个程序开始,一路学习到了:
text
创建 Go Module
→ 编写 main.go
→ go run
→ go build
→ 变量与常量
→ := 与 =
→ 类型推断
→ 零值
→ 作用域
→ 变量遮蔽
→ int 和 float64
→ 数字与字符串转换
这一阶段让我对 Go 最明显的感受是:
Go 不喜欢替开发者猜。
变量类型确定以后,不能随意改变。
不同数字类型的变量不能直接计算。
浮点数转换成整数时,小数部分会被直接截去。
整数转换成字符串,也不能看到 string() 就直接套用 JavaScript 中 String() 的理解。
刚开始会觉得它有点"较真":
text
不都是数字吗?
为什么还要手动转换?
我就是想改个变量,为什么不能继续使用 :=?
明明写了一个变量,暂时没用为什么也不行?
但这些报错也在不断提醒我:
text
这个变量到底是什么类型?
这里是在声明变量,还是在修改变量?
类型转换以后会不会丢失数据?
用户传入的字符串真的可以转换吗?
这次转换到底成功了,还是只得到了一个零值?
这些问题在写页面时可能不一定经常出现,但在处理后端数据时,却很难绕开。
用户从请求中传来的参数,很多时候本来就是字符串:
text
?page=1
&size=20
&price=19.99
如果后端想用这些数据完成分页、计算和数据库查询,就必须先完成转换,并处理可能发生的错误。
目前,我已经在 strconv.Atoi 和 strconv.ParseFloat 中多次写过:
go
value, err := ...
但我还没有正式弄清楚:
text
一个函数为什么可以返回多个值?
err 到底是什么类型?
nil 表示什么?
为什么 Go 代码里到处都是 if err != nil?
return 除了结束函数,还能返回什么?
所以下一篇,将正式进入:
text
函数
→ 参数与返回值
→ 多返回值
→ error
→ nil
→ 错误处理
下一篇见。