Go语言基础语法
1. 文档信息
- 阶段:第一阶段:基础入门
- 预计学习时间:6小时
- 前置知识 :
- 已阅读《Go语言简介》
- 已完成《开发环境搭建》
- 具备基本的编程概念
- 学习目标 :
- 掌握Go语言的基本语法规则
- 理解变量和常量的声明和使用
- 掌握基本的输入输出操作
- 理解Go的命名规范和代码风格
- 能够编写符合Go规范的代码
2. 引言
2.1 为什么需要学习这个主题?
💡 问题场景:你已经搭建好了Go开发环境,迫不及待想要开始编写代码。但是,你面临以下问题:
- Go的语法和我熟悉的语言有什么不同?
- 如何声明变量?有几种方式?
- 什么是短变量声明?什么时候使用它?
- Go的命名规范是什么?
- 如何进行输入输出操作?
- 为什么Go没有分号?
传统语言的复杂性:
许多编程语言的语法规则复杂,有多种方式完成同一件事:
- C/C++:多种变量声明方式,复杂的指针语法
- Java:冗长的类型声明,必须写在类中
- Python:动态类型,缺少编译时检查
- JavaScript:灵活但容易出错的类型系统
Go的解决方案:
Go语言的设计哲学是"简单",体现在语法上:
- 简洁的语法:只有25个关键字
- 统一的风格:gofmt强制统一代码格式
- 明确的规则:一种正确的方式做事
- 编译时检查:未使用的变量和导入会报错
- 自动推导 :
:=短变量声明自动推导类型
2.2 本章学习内容
本章将学习以下内容:
-
Go程序结构
- package声明
- import导入
- 函数定义
- 注释规范
-
变量声明
- var关键字声明
- 短变量声明(:=)
- 多变量声明
- 零值概念
-
常量声明
- const关键字
- iota枚举器
- 常量表达式
-
命名规范
- 标识符规则
- 导出和未导出
- 命名风格
-
基本输入输出
- fmt包的使用
- Print系列函数
- Scan系列函数
- 格式化占位符
-
代码组织
- 代码块和作用域
- 包的组织
- 文件结构
2.3 知识导图
下图展示了本章涉及的主要知识点及其关系:
Go基础语法
程序结构
package声明
import导入
函数定义
注释
变量
var声明
短变量声明
多变量声明
零值
常量
const声明
iota枚举
常量表达式
命名规范
标识符规则
导出规则
命名风格
输入输出
fmt包
Print系列
Scan系列
格式化
代码组织
作用域
包组织
文件结构
从图中可以看出,Go的基础语法主要包括六个方面:程序结构、变量、常量、命名规范、输入输出和代码组织。这些内容是编写Go程序的基础。
3. 核心概念
3.1 Go程序结构
定义和解释
一个标准的Go程序由以下部分组成:
- package声明:每个Go文件必须属于一个包
- import语句:导入需要使用的包
- 函数、变量、常量、类型声明:程序的主体内容
- 注释:代码说明
最小的Go程序:
go
package main
func main() {
}
为什么需要它?
问题背景:
在编写程序时,我们需要:
- 组织代码:将相关功能放在一起
- 重用代码:导入其他包的功能
- 程序入口:明确程序从哪里开始执行
- 代码说明:让其他人理解代码的作用
Go的解决方案:
Go通过清晰的程序结构解决这些问题:
package:组织代码import:重用代码main函数:程序入口- 注释:代码文档
基础示例
go
// 文件名:hello.go
// 这是一个完整的Go程序示例
// package声明:声明当前文件属于哪个包
// main包是特殊的,表示这是一个可执行程序
package main
// import语句:导入需要使用的包
// fmt包提供格式化输入输出功能
import "fmt"
// main函数:程序的入口点
// 当程序运行时,会自动执行main函数
func main() {
// 调用fmt包的Println函数输出文本
// Println会自动添加换行符
fmt.Println("Hello, Go!")
}
// 运行方式:
// go run hello.go
// 输出:
// Hello, Go!
工作原理
Go程序的执行流程:
是
否
开始
读取package声明
处理import语句
查找main包
是否存在main函数?
执行main函数
编译错误
执行函数体代码
程序结束
报错退出
使用场景
- 可执行程序:package main + main函数
- 库包:package 其他名称,供其他程序导入使用
- 测试文件:package xxx_test,用于测试
3.2 变量和零值
定义和解释
变量:用于存储数据的容器,有名称、类型和值。
零值:Go中每种类型都有默认的零值,声明变量时如果不初始化,会自动赋予零值。
常见类型的零值:
- 数值类型:
0 - 布尔类型:
false - 字符串类型:
""(空字符串) - 指针、切片、映射、通道、函数、接口:
nil
为什么需要它?
问题背景:
在其他语言中,未初始化的变量可能包含随机值,导致:
- 程序行为不可预测
- 难以调试的bug
- 安全漏洞
Go的解决方案:
Go保证所有变量都有明确的初始值(零值),这样:
- 程序行为可预测
- 减少bug
- 代码更安全
基础示例
go
package main
import "fmt"
func main() {
// 示例1:声明变量但不初始化,会自动赋予零值
var age int // 零值:0
var name string // 零值:""
var isStudent bool // 零值:false
fmt.Printf("age = %d\n", age) // 输出:age = 0
fmt.Printf("name = %q\n", name) // 输出:name = ""
fmt.Printf("isStudent = %t\n", isStudent) // 输出:isStudent = false
// 示例2:声明并初始化变量
var score int = 95
var city string = "北京"
var graduated bool = true
fmt.Printf("score = %d\n", score) // 输出:score = 95
fmt.Printf("city = %s\n", city) // 输出:city = 北京
fmt.Printf("graduated = %t\n", graduated) // 输出:graduated = true
// 示例3:类型推导(省略类型)
var height = 175 // 自动推导为int
var weight = 65.5 // 自动推导为float64
var country = "中国" // 自动推导为string
fmt.Printf("height类型:%T, 值:%d\n", height, height)
fmt.Printf("weight类型:%T, 值:%.1f\n", weight, weight)
fmt.Printf("country类型:%T, 值:%s\n", country, country)
// 示例4:短变量声明(最常用)
// 使用 := 可以省略var关键字,自动推导类型
email := "user@example.com"
phone := "13800138000"
age2 := 25
fmt.Printf("email = %s\n", email)
fmt.Printf("phone = %s\n", phone)
fmt.Printf("age2 = %d\n", age2)
}
// 输出:
// age = 0
// name = ""
// isStudent = false
// score = 95
// city = 北京
// graduated = true
// height类型:int, 值:175
// weight类型:float64, 值:65.5
// country类型:string, 值:中国
// email = user@example.com
// phone = 13800138000
// age2 = 25
使用场景
- var声明:包级别变量、需要明确类型时
- 短变量声明(:=):函数内部、类型明显时(最常用)
- 零值:初始化数据结构、重置变量
4. 代码示例详解
4.1 基础示例
示例1:Hello World详解
问题描述:理解最简单的Go程序的每个部分。
完整代码:
go
// 文件名:hello_world.go
// 功能:输出Hello World
// 1. package声明
// 每个Go文件都必须以package声明开始
// main包表示这是一个可执行程序(而不是库)
package main
// 2. import语句
// 导入标准库中的fmt包,用于格式化输入输出
// fmt是"format"的缩写
import "fmt"
// 3. main函数
// main函数是程序的入口点
// 程序运行时会自动调用main函数
// main函数没有参数,也没有返回值
func main() {
// 4. 函数调用
// fmt.Println是fmt包中的函数
// Println = Print Line,打印一行并换行
fmt.Println("Hello, World!")
// 可以打印多个值,用逗号分隔
fmt.Println("Hello", "Go", "Language")
// Print不换行,Println换行
fmt.Print("Hello ")
fmt.Print("World\n") // 需要手动添加换行符\n
}
// 运行方式:
// go run hello_world.go
// 输出:
// Hello, World!
// Hello Go Language
// Hello World
代码解释:
package main:声明这是main包,可执行程序必须有main包import "fmt":导入fmt包,提供输入输出功能func main():定义main函数,程序入口fmt.Println():调用fmt包的Println函数输出内容
常见陷阱:
- ⚠️ 可执行程序必须有
package main和func main() - ⚠️ 导入的包如果不使用会报错
- ⚠️
main函数不能有参数和返回值
示例2:变量声明的四种方式
问题描述:Go中有多种变量声明方式,如何选择?
完整代码:
go
package main
import "fmt"
// 方式1:标准声明(包级别变量)
// 在函数外部声明的变量称为包级别变量
var globalName string = "全局变量"
var globalAge int = 30
// 方式2:类型推导(包级别)
var globalCity = "北京" // 自动推导为string
// 方式3:批量声明(包级别)
var (
globalCountry = "中国"
globalPopulation = 1400000000
globalArea = 9600000.0
)
func main() {
fmt.Println("=== 包级别变量 ===")
fmt.Printf("globalName: %s\n", globalName)
fmt.Printf("globalAge: %d\n", globalAge)
fmt.Printf("globalCity: %s\n", globalCity)
fmt.Println("\n=== 函数内变量声明 ===")
// 方式1:标准声明
var name string = "张三"
var age int = 25
fmt.Printf("name: %s, age: %d\n", name, age)
// 方式2:类型推导
var height = 175 // int
var weight = 65.5 // float64
var isStudent = true // bool
fmt.Printf("height: %d, weight: %.1f, isStudent: %t\n", height, weight, isStudent)
// 方式3:短变量声明(最常用)
// 只能在函数内使用
email := "zhangsan@example.com"
phone := "13800138000"
score := 95.5
fmt.Printf("email: %s, phone: %s, score: %.1f\n", email, phone, score)
// 方式4:批量声明
var (
address = "北京市朝阳区"
zipCode = "100000"
company = "科技公司"
)
fmt.Printf("address: %s, zipCode: %s, company: %s\n", address, zipCode, company)
// 方式5:多变量同时声明
var x, y, z int = 1, 2, 3
fmt.Printf("x: %d, y: %d, z: %d\n", x, y, z)
// 方式6:多变量短声明(不同类型)
name2, age2, height2 := "李四", 28, 180
fmt.Printf("name2: %s, age2: %d, height2: %d\n", name2, age2, height2)
fmt.Println("\n=== 零值示例 ===")
// 声明但不初始化,会自动赋予零值
var num int
var str string
var flag bool
fmt.Printf("num零值: %d\n", num) // 0
fmt.Printf("str零值: %q\n", str) // ""
fmt.Printf("flag零值: %t\n", flag) // false
}
// 输出:
// === 包级别变量 ===
// globalName: 全局变量
// globalAge: 30
// globalCity: 北京
//
// === 函数内变量声明 ===
// name: 张三, age: 25
// height: 175, weight: 65.5, isStudent: true
// email: zhangsan@example.com, phone: 13800138000, score: 95.5
// address: 北京市朝阳区, zipCode: 100000, company: 科技公司
// x: 1, y: 2, z: 3
// name2: 李四, age2: 28, height2: 180
//
// === 零值示例 ===
// num零值: 0
// str零值: ""
// flag零值: false
代码解释:
- var声明:适用于包级别变量或需要明确类型的场景
- 短变量声明(:=):只能在函数内使用,最常用
- 批量声明 :使用
var ()声明多个变量 - 零值:未初始化的变量自动赋予零值
常见陷阱:
- ⚠️
:=只能在函数内使用,包级别必须用var - ⚠️ 短变量声明至少要声明一个新变量
- ⚠️ 已声明的变量不能用
:=重复声明
示例3:常量声明
问题描述:如何声明和使用常量?
完整代码:
go
package main
import "fmt"
// 包级别常量
const Pi = 3.14159
const AppName = "我的应用"
// 批量声明常量
const (
StatusOK = 200
StatusNotFound = 404
StatusInternalError = 500
)
// 使用iota枚举器
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// iota高级用法:位运算
const (
ReadPermission = 1 << iota // 1 << 0 = 1
WritePermission // 1 << 1 = 2
ExecutePermission // 1 << 2 = 4
)
func main() {
fmt.Println("=== 基本常量 ===")
fmt.Printf("Pi = %.5f\n", Pi)
fmt.Printf("AppName = %s\n", AppName)
fmt.Println("\n=== HTTP状态码 ===")
fmt.Printf("StatusOK = %d\n", StatusOK)
fmt.Printf("StatusNotFound = %d\n", StatusNotFound)
fmt.Printf("StatusInternalError = %d\n", StatusInternalError)
fmt.Println("\n=== 星期枚举 ===")
fmt.Printf("Sunday = %d\n", Sunday)
fmt.Printf("Monday = %d\n", Monday)
fmt.Printf("Friday = %d\n", Friday)
fmt.Printf("Saturday = %d\n", Saturday)
fmt.Println("\n=== 权限位 ===")
fmt.Printf("ReadPermission = %d (二进制: %b)\n", ReadPermission, ReadPermission)
fmt.Printf("WritePermission = %d (二进制: %b)\n", WritePermission, WritePermission)
fmt.Printf("ExecutePermission = %d (二进制: %b)\n", ExecutePermission, ExecutePermission)
// 组合权限
fullPermission := ReadPermission | WritePermission | ExecutePermission
fmt.Printf("FullPermission = %d (二进制: %b)\n", fullPermission, fullPermission)
fmt.Println("\n=== 函数内常量 ===")
const localConst = "局部常量"
const (
maxRetry = 3
timeout = 30
)
fmt.Printf("localConst = %s\n", localConst)
fmt.Printf("maxRetry = %d, timeout = %d\n", maxRetry, timeout)
// 常量表达式
const (
a = 10
b = 20
c = a + b // 常量表达式在编译时计算
)
fmt.Printf("a + b = %d\n", c)
}
// 输出:
// === 基本常量 ===
// Pi = 3.14159
// AppName = 我的应用
//
// === HTTP状态码 ===
// StatusOK = 200
// StatusNotFound = 404
// StatusInternalError = 500
//
// === 星期枚举 ===
// Sunday = 0
// Monday = 1
// Friday = 5
// Saturday = 6
//
// === 权限位 ===
// ReadPermission = 1 (二进制: 1)
// WritePermission = 2 (二进制: 10)
// ExecutePermission = 4 (二进制: 100)
// FullPermission = 7 (二进制: 111)
//
// === 函数内常量 ===
// localConst = 局部常量
// maxRetry = 3, timeout = 30
// a + b = 30
代码解释:
- const:声明常量,值在编译时确定,运行时不可修改
- iota:常量生成器,从0开始自动递增
- 位运算:使用iota和位移运算创建标志位
- 常量表达式:常量可以参与编译时计算
常见陷阱:
- ⚠️ 常量不能用
:=声明 - ⚠️ 常量的值必须在编译时确定
- ⚠️ iota在每个const块中重新从0开始
示例4:格式化输出
问题描述:如何使用fmt包进行格式化输出?
完整代码:
go
package main
import "fmt"
func main() {
// 基本数据
name := "张三"
age := 25
height := 175.5
isStudent := true
score := 95.8
fmt.Println("=== Print系列函数 ===")
// Print:输出内容,不换行
fmt.Print("Hello ")
fmt.Print("World\n")
// Println:输出内容并换行
fmt.Println("Hello", "Go")
// Printf:格式化输出
fmt.Printf("姓名:%s,年龄:%d岁\n", name, age)
fmt.Println("\n=== 常用格式化占位符 ===")
// %v:默认格式
fmt.Printf("%%v: %v\n", name)
// %+v:结构体会显示字段名
// %#v:Go语法表示
fmt.Printf("%%#v: %#v\n", name)
// %T:类型
fmt.Printf("%%T: %T\n", name)
// %d:十进制整数
fmt.Printf("%%d: %d\n", age)
// %b:二进制
fmt.Printf("%%b: %b\n", age)
// %o:八进制
fmt.Printf("%%o: %o\n", age)
// %x:十六进制(小写)
fmt.Printf("%%x: %x\n", age)
// %X:十六进制(大写)
fmt.Printf("%%X: %X\n", age)
// %f:浮点数
fmt.Printf("%%f: %f\n", height)
// %.2f:保留2位小数
fmt.Printf("%%.2f: %.2f\n", height)
// %e:科学计数法
fmt.Printf("%%e: %e\n", height)
// %s:字符串
fmt.Printf("%%s: %s\n", name)
// %q:带引号的字符串
fmt.Printf("%%q: %q\n", name)
// %t:布尔值
fmt.Printf("%%t: %t\n", isStudent)
// %p:指针地址
fmt.Printf("%%p: %p\n", &name)
fmt.Println("\n=== 宽度和精度 ===")
// %5d:宽度为5,右对齐
fmt.Printf("|%5d|\n", age)
// %-5d:宽度为5,左对齐
fmt.Printf("|%-5d|\n", age)
// %05d:宽度为5,用0填充
fmt.Printf("|%05d|\n", age)
// %8.2f:总宽度8,小数点后2位
fmt.Printf("|%8.2f|\n", score)
// %10s:宽度为10
fmt.Printf("|%10s|\n", name)
// %-10s:宽度为10,左对齐
fmt.Printf("|%-10s|\n", name)
fmt.Println("\n=== Sprintf:格式化为字符串 ===")
// Sprintf返回格式化后的字符串,不输出
message := fmt.Sprintf("姓名:%s,年龄:%d岁,身高:%.1fcm", name, age, height)
fmt.Println(message)
// Sprintln:格式化为字符串并添加换行
message2 := fmt.Sprintln("Hello", "Go")
fmt.Print(message2)
fmt.Println("\n=== 多个值的输出 ===")
// 输出多个值
fmt.Printf("姓名:%s,年龄:%d,身高:%.1f,学生:%t,分数:%.1f\n",
name, age, height, isStudent, score)
// 使用索引(重复使用参数)
fmt.Printf("%[1]s今年%[2]d岁,%[1]s是学生\n", name, age)
}
// 输出:
// === Print系列函数 ===
// Hello World
// Hello Go
// 姓名:张三,年龄:25岁
//
// === 常用格式化占位符 ===
// %v: 张三
// %#v: "张三"
// %T: string
// %d: 25
// %b: 11001
// %o: 31
// %x: 19
// %X: 19
// %f: 175.500000
// %.2f: 175.50
// %e: 1.755000e+02
// %s: 张三
// %q: "张三"
// %t: true
// %p: 0xc000010230
//
// === 宽度和精度 ===
// | 25|
// |25 |
// |00025|
// | 95.80|
// | 张三|
// |张三 |
//
// === Sprintf:格式化为字符串 ===
// 姓名:张三,年龄:25岁,身高:175.5cm
// Hello Go
//
// === 多个值的输出 ===
// 姓名:张三,年龄:25,身高:175.5,学生:true,分数:95.8
// 张三今年25岁,张三是学生
代码解释:
- Print:直接输出,不换行
- Println:输出并换行
- Printf:格式化输出
- Sprintf:格式化为字符串
- 占位符:%v、%d、%f、%s、%t等
常见陷阱:
- ⚠️ 占位符数量要与参数数量匹配
- ⚠️ 占位符类型要与参数类型匹配
- ⚠️
%v是通用占位符,但不如具体类型清晰
示例5:输入操作
问题描述:如何从控制台读取用户输入?
完整代码:
go
package main
import "fmt"
func main() {
fmt.Println("=== Scan系列函数 ===")
// 示例1:Scan - 读取空格分隔的输入
fmt.Println("\n请输入姓名和年龄(用空格分隔):")
var name string
var age int
// Scan会读取输入,遇到空格或换行停止
// 返回成功读取的参数个数和错误
n, err := fmt.Scan(&name, &age)
if err != nil {
fmt.Println("输入错误:", err)
} else {
fmt.Printf("成功读取%d个参数\n", n)
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
}
// 示例2:Scanln - 读取一行
fmt.Println("\n请输入城市名称:")
var city string
fmt.Scanln(&city)
fmt.Printf("城市:%s\n", city)
// 示例3:Scanf - 格式化输入
fmt.Println("\n请按格式输入(姓名:年龄):")
var name2 string
var age2 int
fmt.Scanf("%s:%d", &name2, &age2)
fmt.Printf("姓名:%s,年龄:%d\n", name2, age2)
// 示例4:读取多个值
fmt.Println("\n请输入三个整数(用空格分隔):")
var a, b, c int
fmt.Scan(&a, &b, &c)
fmt.Printf("a=%d, b=%d, c=%d, 和=%d\n", a, b, c, a+b+c)
// 示例5:读取浮点数
fmt.Println("\n请输入身高(米)和体重(公斤):")
var height, weight float64
fmt.Scan(&height, &weight)
bmi := weight / (height * height)
fmt.Printf("身高:%.2fm,体重:%.1fkg,BMI:%.2f\n", height, weight, bmi)
}
// 运行示例:
// go run input.go
// 交互示例:
// === Scan系列函数 ===
//
// 请输入姓名和年龄(用空格分隔):
// 张三 25
// 成功读取2个参数
// 姓名:张三,年龄:25
//
// 请输入城市名称:
// 北京
// 城市:北京
//
// 请按格式输入(姓名:年龄):
// 李四:30
// 姓名:李四,年龄:30
//
// 请输入三个整数(用空格分隔):
// 10 20 30
// a=10, b=20, c=30, 和=60
//
// 请输入身高(米)和体重(公斤):
// 1.75 70
// 身高:1.75m,体重:70.0kg,BMI:22.86
代码解释:
- Scan:读取空格分隔的输入
- Scanln:读取一行输入
- Scanf:按格式读取输入
- &变量:传递变量的地址,以便修改变量的值
常见陷阱:
- ⚠️ Scan函数需要传递变量的地址(&变量)
- ⚠️ 输入类型要与变量类型匹配
- ⚠️ Scan遇到空格会停止读取当前变量
4.2 进阶示例
示例6:作用域和变量遮蔽
问题描述:理解Go中的作用域规则和变量遮蔽现象。
完整代码:
go
package main
import "fmt"
// 包级别变量
var globalVar = "全局变量"
func main() {
fmt.Println("=== 作用域示例 ===")
// 函数级别变量
var localVar = "函数变量"
fmt.Printf("globalVar: %s\n", globalVar)
fmt.Printf("localVar: %s\n", localVar)
// 代码块作用域
{
// 块级别变量
var blockVar = "块变量"
fmt.Printf("blockVar: %s\n", blockVar)
// 可以访问外层变量
fmt.Printf("访问外层localVar: %s\n", localVar)
}
// blockVar在这里不可访问
// fmt.Println(blockVar) // 编译错误:undefined: blockVar
fmt.Println("\n=== 变量遮蔽示例 ===")
x := 10
fmt.Printf("外层x: %d\n", x)
{
// 内层声明同名变量,遮蔽外层变量
x := 20
fmt.Printf("内层x: %d\n", x)
// 修改内层x不影响外层x
x = 30
fmt.Printf("修改后内层x: %d\n", x)
}
// 外层x未被修改
fmt.Printf("外层x仍然是: %d\n", x)
fmt.Println("\n=== if语句作用域 ===")
// if语句可以包含初始化语句
if y := 100; y > 50 {
fmt.Printf("if块内y: %d\n", y)
// y只在if块内有效
}
// y在这里不可访问
// fmt.Println(y) // 编译错误:undefined: y
fmt.Println("\n=== for循环作用域 ===")
// for循环变量的作用域
for i := 0; i < 3; i++ {
fmt.Printf("循环内i: %d\n", i)
}
// i在这里不可访问
// fmt.Println(i) // 编译错误:undefined: i
fmt.Println("\n=== 包级别变量遮蔽 ===")
// 函数内声明同名变量,遮蔽包级别变量
globalVar := "局部globalVar"
fmt.Printf("局部globalVar: %s\n", globalVar)
// 如何访问被遮蔽的包级别变量?
// 在Go中,没有直接方法访问被遮蔽的包级别变量
// 最好避免这种情况
}
// 输出:
// === 作用域示例 ===
// globalVar: 全局变量
// localVar: 函数变量
// blockVar: 块变量
// 访问外层localVar: 函数变量
//
// === 变量遮蔽示例 ===
// 外层x: 10
// 内层x: 20
// 修改后内层x: 30
// 外层x仍然是: 10
//
// === if语句作用域 ===
// if块内y: 100
//
// === for循环作用域 ===
// 循环内i: 0
// 循环内i: 1
// 循环内i: 2
//
// === 包级别变量遮蔽 ===
// 局部globalVar: 局部globalVar
代码解释:
- 作用域:变量的可见范围
- 变量遮蔽:内层变量遮蔽外层同名变量
- 块作用域 :
{}创建新的作用域 - 语句作用域:if、for等语句可以有自己的作用域
常见陷阱:
- ⚠️ 变量遮蔽可能导致意外的bug
- ⚠️ 被遮蔽的包级别变量无法直接访问
- ⚠️ 循环变量在循环外不可访问
示例7:类型转换
问题描述:Go是强类型语言,不同类型之间如何转换?
完整代码:
go
package main
import (
"fmt"
"strconv"
)
func main() {
fmt.Println("=== 数值类型转换 ===")
// int转float64
var i int = 42
var f float64 = float64(i)
fmt.Printf("int %d 转 float64: %.2f\n", i, f)
// float64转int(会截断小数部分)
var f2 float64 = 3.14159
var i2 int = int(f2)
fmt.Printf("float64 %.5f 转 int: %d\n", f2, i2)
// int32转int64
var i32 int32 = 100
var i64 int64 = int64(i32)
fmt.Printf("int32 %d 转 int64: %d\n", i32, i64)
// 不同大小的int类型转换
var a int8 = 127
var b int16 = int16(a)
var c int32 = int32(b)
var d int64 = int64(c)
fmt.Printf("int8(%d) -> int16(%d) -> int32(%d) -> int64(%d)\n", a, b, c, d)
fmt.Println("\n=== 字符串转数值 ===")
// 字符串转int
str1 := "123"
num1, err := strconv.Atoi(str1)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 %q 转 int: %d\n", str1, num1)
}
// 字符串转int64
str2 := "9876543210"
num2, err := strconv.ParseInt(str2, 10, 64)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 %q 转 int64: %d\n", str2, num2)
}
// 字符串转float64
str3 := "3.14159"
num3, err := strconv.ParseFloat(str3, 64)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 %q 转 float64: %.5f\n", str3, num3)
}
// 字符串转bool
str4 := "true"
bool1, err := strconv.ParseBool(str4)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 %q 转 bool: %t\n", str4, bool1)
}
fmt.Println("\n=== 数值转字符串 ===")
// int转字符串
num4 := 456
str5 := strconv.Itoa(num4)
fmt.Printf("int %d 转字符串: %q\n", num4, str5)
// int64转字符串
num5 := int64(123456789)
str6 := strconv.FormatInt(num5, 10)
fmt.Printf("int64 %d 转字符串: %q\n", num5, str6)
// float64转字符串
num6 := 3.14159
str7 := strconv.FormatFloat(num6, 'f', 2, 64)
fmt.Printf("float64 %.5f 转字符串: %q\n", num6, str7)
// bool转字符串
bool2 := true
str8 := strconv.FormatBool(bool2)
fmt.Printf("bool %t 转字符串: %q\n", bool2, str8)
fmt.Println("\n=== 使用fmt.Sprintf转换 ===")
// 任意类型转字符串
num7 := 789
str9 := fmt.Sprintf("%d", num7)
fmt.Printf("使用Sprintf: %q\n", str9)
num8 := 2.71828
str10 := fmt.Sprintf("%.3f", num8)
fmt.Printf("使用Sprintf: %q\n", str10)
fmt.Println("\n=== 转换错误处理 ===")
// 无效的字符串转int
invalidStr := "abc"
_, err = strconv.Atoi(invalidStr)
if err != nil {
fmt.Printf("转换 %q 失败: %v\n", invalidStr, err)
}
// 溢出
overflowStr := "999999999999999999999"
_, err = strconv.Atoi(overflowStr)
if err != nil {
fmt.Printf("转换 %q 失败: %v\n", overflowStr, err)
}
}
// 输出:
// === 数值类型转换 ===
// int 42 转 float64: 42.00
// float64 3.14159 转 int: 3
// int32 100 转 int64: 100
// int8(127) -> int16(127) -> int32(127) -> int64(127)
//
// === 字符串转数值 ===
// 字符串 "123" 转 int: 123
// 字符串 "9876543210" 转 int64: 9876543210
// 字符串 "3.14159" 转 float64: 3.14159
// 字符串 "true" 转 bool: true
//
// === 数值转字符串 ===
// int 456 转字符串: "456"
// int64 123456789 转字符串: "123456789"
// float64 3.14159 转字符串: "3.14"
// bool true 转字符串: "true"
//
// === 使用fmt.Sprintf转换 ===
// 使用Sprintf: "789"
// 使用Sprintf: "2.718"
//
// === 转换错误处理 ===
// 转换 "abc" 失败: strconv.Atoi: parsing "abc": invalid syntax
// 转换 "999999999999999999999" 失败: strconv.Atoi: parsing "999999999999999999999": value out of range
代码解释:
- 类型转换 :使用
类型(值)语法 - strconv包:字符串和基本类型之间的转换
- 错误处理:转换可能失败,需要检查错误
- fmt.Sprintf:通用的转字符串方法
常见陷阱:
- ⚠️ float转int会截断小数部分,不是四舍五入
- ⚠️ 字符串转数值可能失败,必须检查错误
- ⚠️ 不同大小的整数类型转换可能溢出
示例8:命名规范实践
问题描述:Go的命名规范是什么?如何写出符合规范的代码?
完整代码:
go
package main
import "fmt"
// ✅ 好的命名:大写字母开头,表示导出(public)
// 其他包可以访问
const MaxConnections = 100
const DefaultTimeout = 30
// ✅ 好的命名:小写字母开头,表示未导出(private)
// 只能在当前包内访问
const minRetry = 3
const maxRetry = 10
// ✅ 好的命名:使用驼峰命名法
var userCount int
var totalAmount float64
var isConnected bool
// ❌ 不好的命名:使用下划线(不符合Go风格)
// var user_count int
// var total_amount float64
// ✅ 好的命名:缩写词全部大写或全部小写
var userID int // ID全部大写
var httpServer string // HTTP全部大写
var urlPath string // URL全部大写
// ❌ 不好的命名:缩写词大小写混合
// var userId int
// var httpServer string
// ✅ 好的函数命名:动词开头,描述功能
func GetUserName() string {
return "张三"
}
func SetUserAge(age int) {
fmt.Printf("设置年龄:%d\n", age)
}
func IsValidEmail(email string) bool {
return len(email) > 0
}
func CalculateTotal(price, quantity int) int {
return price * quantity
}
// ✅ 好的命名:接收者使用类型的首字母(小写)
type User struct {
Name string
Age int
}
// 接收者命名为u(User的首字母)
func (u User) GetInfo() string {
return fmt.Sprintf("%s, %d岁", u.Name, u.Age)
}
// ✅ 好的命名:常量使用有意义的名称
const (
StatusPending = "pending"
StatusApproved = "approved"
StatusRejected = "rejected"
)
// ✅ 好的命名:错误变量以Err开头
var (
ErrNotFound = fmt.Errorf("未找到")
ErrInvalidInput = fmt.Errorf("无效输入")
ErrTimeout = fmt.Errorf("超时")
)
func main() {
fmt.Println("=== 命名规范示例 ===")
// ✅ 好的变量命名:简短但有意义
name := "李四"
age := 30
email := "lisi@example.com"
fmt.Printf("姓名:%s,年龄:%d,邮箱:%s\n", name, age, email)
// ✅ 好的命名:循环变量使用i, j, k
for i := 0; i < 3; i++ {
fmt.Printf("循环 %d\n", i)
}
// ✅ 好的命名:range循环使用有意义的名称
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
fmt.Printf("索引:%d,值:%d\n", index, value)
}
// ✅ 好的命名:不使用的变量用_忽略
for _, value := range numbers {
fmt.Printf("值:%d\n", value)
}
// ✅ 好的命名:布尔变量以is, has, can等开头
isValid := true
hasPermission := false
canEdit := true
fmt.Printf("isValid: %t, hasPermission: %t, canEdit: %t\n",
isValid, hasPermission, canEdit)
// ✅ 好的命名:使用完整单词,避免缩写
// 除非是广为人知的缩写(如ID, URL, HTTP)
userName := "王五" // ✅ 好
// usrName := "王五" // ❌ 不好
userID := 12345 // ✅ 好(ID是常见缩写)
// userId := 12345 // ❌ 不好(大小写不对)
fmt.Printf("userName: %s, userID: %d\n", userName, userID)
fmt.Println("\n=== 包级别标识符 ===")
// 导出的标识符(大写开头)
fmt.Printf("MaxConnections: %d\n", MaxConnections)
fmt.Printf("DefaultTimeout: %d\n", DefaultTimeout)
// 未导出的标识符(小写开头)
fmt.Printf("minRetry: %d\n", minRetry)
fmt.Printf("maxRetry: %d\n", maxRetry)
fmt.Println("\n=== 函数命名 ===")
// 调用导出的函数
userName2 := GetUserName()
fmt.Printf("用户名:%s\n", userName2)
SetUserAge(25)
isValid2 := IsValidEmail("test@example.com")
fmt.Printf("邮箱有效:%t\n", isValid2)
total := CalculateTotal(100, 5)
fmt.Printf("总计:%d\n", total)
fmt.Println("\n=== 结构体和方法 ===")
user := User{Name: "赵六", Age: 28}
info := user.GetInfo()
fmt.Println(info)
}
// 输出:
// === 命名规范示例 ===
// 姓名:李四,年龄:30,邮箱:lisi@example.com
// 循环 0
// 循环 1
// 循环 2
// 索引:0,值:1
// 索引:1,值:2
// 索引:2,值:3
// 索引:3,值:4
// 索引:4,值:5
// 值:1
// 值:2
// 值:3
// 值:4
// 值:5
// isValid: true, hasPermission: false, canEdit: true
// userName: 王五, userID: 12345
//
// === 包级别标识符 ===
// MaxConnections: 100
// DefaultTimeout: 30
// minRetry: 3
// maxRetry: 10
//
// === 函数命名 ===
// 用户名:张三
// 设置年龄:25
// 邮箱有效:true
// 总计:500
//
// === 结构体和方法 ===
// 赵六, 28岁
代码解释:
- 导出规则:大写开头导出,小写开头未导出
- 驼峰命名:使用驼峰而不是下划线
- 缩写词:全部大写或全部小写
- 有意义的名称:避免无意义的缩写
常见陷阱:
- ⚠️ 不要使用下划线命名(除了特殊情况如
_忽略变量) - ⚠️ 缩写词要么全大写要么全小写,不要混合
- ⚠️ 导出的标识符要有文档注释
示例9:注释规范
问题描述:如何写出规范的Go注释?
完整代码:
go
// Package main 是程序的入口包
// 这个程序演示了Go语言的注释规范
package main
import "fmt"
// 常量定义
// MaxRetry 定义最大重试次数
const MaxRetry = 3
// DefaultPort 定义默认端口号
const DefaultPort = 8080
/*
批量常量定义
这些常量用于表示用户状态
*/
const (
// StatusActive 表示用户处于活跃状态
StatusActive = "active"
// StatusInactive 表示用户处于非活跃状态
StatusInactive = "inactive"
// StatusBanned 表示用户被禁用
StatusBanned = "banned"
)
// User 表示系统用户
// 包含用户的基本信息
type User struct {
// Name 是用户的姓名
Name string
// Age 是用户的年龄
Age int
// Email 是用户的电子邮箱
Email string
}
// NewUser 创建一个新的用户实例
// 参数:
// - name: 用户姓名
// - age: 用户年龄
// - email: 用户邮箱
//
// 返回值:
// - *User: 新创建的用户指针
//
// 示例:
//
// user := NewUser("张三", 25, "zhangsan@example.com")
func NewUser(name string, age int, email string) *User {
return &User{
Name: name,
Age: age,
Email: email,
}
}
// GetInfo 返回用户的信息字符串
// 格式:姓名 (年龄岁) - 邮箱
func (u *User) GetInfo() string {
return fmt.Sprintf("%s (%d岁) - %s", u.Name, u.Age, u.Email)
}
// Validate 验证用户信息是否有效
// 返回true表示有效,false表示无效
func (u *User) Validate() bool {
// 检查姓名是否为空
if u.Name == "" {
return false
}
// 检查年龄是否在合理范围内
if u.Age < 0 || u.Age > 150 {
return false
}
// 检查邮箱是否为空
if u.Email == "" {
return false
}
return true
}
// calculateBMI 计算BMI指数(未导出函数)
// 这是一个内部辅助函数
func calculateBMI(weight, height float64) float64 {
// BMI = 体重(kg) / 身高(m)²
return weight / (height * height)
}
func main() {
fmt.Println("=== 注释规范示例 ===")
// 创建用户
user := NewUser("李四", 30, "lisi@example.com")
// 输出用户信息
fmt.Println(user.GetInfo())
// 验证用户信息
if user.Validate() {
fmt.Println("用户信息有效")
} else {
fmt.Println("用户信息无效")
}
/*
多行注释示例
可以用于较长的说明
但通常推荐使用多个单行注释
*/
// TODO: 添加更多用户验证逻辑
// FIXME: 邮箱格式验证需要改进
// NOTE: 这里可以添加日志记录
// 计算BMI
weight := 70.0 // 体重(公斤)
height := 1.75 // 身高(米)
bmi := calculateBMI(weight, height)
fmt.Printf("BMI: %.2f\n", bmi)
}
// 输出:
// === 注释规范示例 ===
// 李四 (30岁) - lisi@example.com
// 用户信息有效
// BMI: 22.86
代码解释:
- 包注释:在package语句前,描述包的功能
- 导出标识符注释:必须以标识符名称开头
- 函数注释:描述功能、参数、返回值
- 行内注释:解释复杂逻辑
注释最佳实践:
- 所有导出的标识符都要有注释
- 注释要以标识符名称开头
- 使用完整的句子,首字母大写,句号结尾
- 避免无意义的注释
- 使用TODO、FIXME、NOTE等标记
常见陷阱:
- ⚠️ 导出的标识符缺少注释会被golint警告
- ⚠️ 注释不要以标识符名称开头会被警告
- ⚠️ 过时的注释比没有注释更糟糕
示例10:多文件程序
问题描述:如何组织多个Go文件?
文件结构:
myapp/
├── main.go
├── user.go
└── utils.go
main.go:
go
// Package main 是程序的入口包
package main
import "fmt"
func main() {
fmt.Println("=== 多文件程序示例 ===")
// 使用user.go中定义的函数
user := CreateUser("张三", 25)
fmt.Println(GetUserInfo(user))
// 使用utils.go中定义的函数
result := Add(10, 20)
fmt.Printf("10 + 20 = %d\n", result)
message := FormatMessage("Hello", "Go")
fmt.Println(message)
}
user.go:
go
// Package main 的用户相关功能
package main
import "fmt"
// UserInfo 存储用户信息
type UserInfo struct {
Name string
Age int
}
// CreateUser 创建新用户
func CreateUser(name string, age int) UserInfo {
return UserInfo{
Name: name,
Age: age,
}
}
// GetUserInfo 获取用户信息字符串
func GetUserInfo(user UserInfo) string {
return fmt.Sprintf("用户:%s,年龄:%d岁", user.Name, user.Age)
}
utils.go:
go
// Package main 的工具函数
package main
import "fmt"
// Add 计算两个整数的和
func Add(a, b int) int {
return a + b
}
// Subtract 计算两个整数的差
func Subtract(a, b int) int {
return a - b
}
// FormatMessage 格式化消息
func FormatMessage(prefix, content string) string {
return fmt.Sprintf("[%s] %s", prefix, content)
}
运行方式:
bash
# 方式1:运行所有文件
go run main.go user.go utils.go
# 方式2:运行当前目录所有.go文件
go run .
# 方式3:构建可执行文件
go build -o myapp
./myapp
输出:
=== 多文件程序示例 ===
用户:张三,年龄:25岁
10 + 20 = 30
[Hello] Go
代码解释:
- 同一个包的多个文件可以互相访问
- 所有文件必须声明相同的package
- 可以将相关功能分散到不同文件
- 运行时需要指定所有文件或使用
.
常见陷阱:
- ⚠️ 所有文件必须在同一个包中
- ⚠️ 不能有重复的函数或变量定义
- ⚠️
go run需要指定所有相关文件
4.3 高级应用
示例11:包的导入和别名
go
package main
import (
"fmt"
f "fmt" // 别名
. "fmt" // 点导入(不推荐)
_ "image/png" // 仅执行init函数
)
func main() {
fmt.Println("标准导入")
f.Println("使用别名")
Println("点导入(不推荐)")
}
示例12:init函数
go
package main
import "fmt"
var globalVar = initGlobal()
func initGlobal() int {
fmt.Println("1. 初始化全局变量")
return 100
}
func init() {
fmt.Println("2. 执行init函数")
}
func main() {
fmt.Println("3. 执行main函数")
}
示例13:空白标识符
go
package main
import "fmt"
func getValues() (int, string, bool) {
return 42, "hello", true
}
func main() {
// 忽略不需要的返回值
num, _, flag := getValues()
fmt.Printf("num=%d, flag=%t\n", num, flag)
}
示例14:类型别名
go
package main
import "fmt"
type UserID int
type Username string
func main() {
var id UserID = 12345
var name Username = "张三"
fmt.Printf("ID: %d, Name: %s\n", id, name)
}
示例15:常量表达式计算
go
package main
import "fmt"
const (
KB = 1024
MB = 1024 * KB
GB = 1024 * MB
TB = 1024 * GB
)
func main() {
fmt.Printf("1 TB = %d bytes\n", TB)
}
4.4 对比示例
示例16:var vs :=
go
package main
import "fmt"
var globalVar = "包级别" // ✅ 必须用var
func main() {
var localVar1 = "函数内var" // ✅ 可以
localVar2 := "函数内:=" // ✅ 推荐
// := 不能用于包级别
// globalVar2 := "错误" // ❌ 编译错误
fmt.Println(globalVar, localVar1, localVar2)
}
示例17:Print vs Println vs Printf
go
package main
import "fmt"
func main() {
name := "Go"
fmt.Print("Hello ")
fmt.Print(name)
fmt.Print("\n")
fmt.Println("Hello", name)
fmt.Printf("Hello %s\n", name)
}
示例18:显式类型 vs 类型推导
go
package main
import "fmt"
func main() {
var x int = 10 // 显式类型
var y = 10 // 类型推导为int
z := 10 // 类型推导为int
fmt.Printf("x=%d(%T), y=%d(%T), z=%d(%T)\n", x, x, y, y, z, z)
}
示例19:单行注释 vs 多行注释
go
package main
// 单行注释:推荐用于大多数情况
// 可以连续使用多个单行注释
/*
多行注释:
用于较长的说明
或临时注释掉代码块
*/
func main() {
// 这是单行注释
x := 10
/*
这是多行注释
可以跨越多行
*/
y := 20
}
示例20:导出 vs 未导出
go
package main
import "fmt"
// ExportedFunc 可以被其他包访问(大写开头)
func ExportedFunc() {
fmt.Println("导出的函数")
}
// unexportedFunc 只能在当前包内访问(小写开头)
func unexportedFunc() {
fmt.Println("未导出的函数")
}
func main() {
ExportedFunc()
unexportedFunc()
}
5. 问题与解决方案
5.1 问题1:未使用的变量导致编译错误
问题描述 :声明了变量但未使用,导致编译失败。
解决方案:
go
package main
import "fmt"
func main() {
// 使用空白标识符忽略不需要的值
_, err := fmt.Println("Hello")
if err != nil {
return
}
}
5.2 问题2:变量重复声明
问题描述 :使用:=重复声明已存在的变量。
解决方案:
go
package main
import "fmt"
func main() {
x := 10
// x := 20 // 错误:重复声明
x = 20 // 正确:赋值
fmt.Println(x)
}
5.3 问题3:类型不匹配
问题描述 :不同类型的值不能直接运算。
解决方案:
go
package main
import "fmt"
func main() {
var a int = 10
var b float64 = 20.5
// c := a + b // 错误:类型不匹配
c := float64(a) + b // 正确:类型转换
fmt.Println(c)
}
5.4 问题4:导入包未使用
问题描述 :导入的包没有使用会报错。
解决方案:
go
package main
import (
"fmt"
_ "image/png" // 使用_表示只执行init
)
func main() {
fmt.Println("Hello")
}
5.5 问题5:常量不能修改
问题描述 :尝试修改常量的值。
解决方案:
go
package main
func main() {
const x = 10
// x = 20 // 错误:常量不能修改
var y = 10 // 使用变量
y = 20 // 正确
}
6. 最佳实践
6.1 实践1:优先使用短变量声明
好的示例:
go
name := "张三"
age := 25
不好的示例:
go
var name string = "张三"
var age int = 25
6.2 实践2:使用有意义的变量名
好的示例:
go
userCount := 100
totalAmount := 1500.50
不好的示例:
go
x := 100
y := 1500.50
6.3 实践3:使用gofmt格式化代码
bash
gofmt -w *.go
6.4 实践4:为导出的标识符添加注释
go
// MaxRetry 定义最大重试次数
const MaxRetry = 3
6.5 实践5:避免使用点导入
go
// ❌ 不推荐
import . "fmt"
// ✅ 推荐
import "fmt"
7. 常见错误
7.1 错误1:忘记package声明
错误代码:
go
import "fmt"
func main() {
fmt.Println("Hello")
}
正确代码:
go
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
7.2 错误2:main函数有参数或返回值
错误代码:
go
package main
func main(args []string) int {
return 0
}
正确代码:
go
package main
func main() {
// 无参数,无返回值
}
7.3 错误3:变量名使用保留字
错误代码:
go
var for = 10 // for是关键字
正确代码:
go
var count = 10
7.4 错误4:字符串拼接使用+号时类型不匹配
错误代码:
go
age := 25
msg := "年龄:" + age // 错误
正确代码:
go
age := 25
msg := fmt.Sprintf("年龄:%d", age)
7.5 错误5:常量使用:=声明
错误代码:
go
const x := 10 // 错误
正确代码:
go
const x = 10
9. 练习题
9.1 基础练习
练习1:变量声明和输出
编写程序,声明姓名、年龄、身高变量并输出。
练习2:常量定义
定义一周七天的常量,使用iota。
练习3:格式化输出
使用Printf输出个人信息,包含姓名、年龄、身高。
9.2 进阶练习
练习4:类型转换
编写程序,输入身高(厘米),转换为米并计算BMI。
练习5:多文件程序
创建包含main.go和utils.go的程序,实现基本计算功能。
9.3 挑战练习
练习6:命令行参数处理
编写程序,接收命令行参数并进行处理。
10. 思考问题
- 为什么Go要求未使用的变量和导入必须删除?
- 短变量声明:=和var声明各适用于什么场景?
- Go的零值设计有什么优势?
- 为什么Go使用大小写来控制导出而不是public/private关键字?
- iota在实际开发中有哪些应用场景?
11. 总结
11.1 知识回顾
本章学习了Go语言的基础语法,包括程序结构、变量声明、常量定义、命名规范、输入输出等核心内容。
11.2 知识图谱
Go基础语法
程序结构
变量
常量
命名规范
输入输出
代码组织
11.3 下一步学习
12. 参考资料
附录:练习题参考答案
练习1答案
go
package main
import "fmt"
func main() {
name := "张三"
age := 25
height := 175.5
fmt.Printf("姓名:%s,年龄:%d,身高:%.1f\n", name, age, height)
}
练习2答案
go
package main
import "fmt"
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
func main() {
fmt.Println(Sunday, Monday, Friday)
}
练习3答案
go
package main
import "fmt"
func main() {
name := "李四"
age := 30
height := 180.0
fmt.Printf("个人信息:\n姓名:%s\n年龄:%d岁\n身高:%.1fcm\n", name, age, height)
}