跳到主要内容

Go 基础语法

本章将深入介绍 Go 语言的基础语法,包括变量声明、数据类型、运算符和基本控制结构。理解这些基础概念是掌握 Go 语言的关键。

第一个 Go 程序

让我们从经典的 "Hello, World" 程序开始:

package main

import "fmt"

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

代码结构解析

包声明(Package Declaration)

每个 Go 文件都必须以 package 声明开头。包是 Go 代码组织的基本单位:

  • package main 表示这是一个可执行程序的入口包
  • 其他包名(如 package utils)用于组织可复用的代码库
  • 同一个目录下的所有文件必须属于同一个包

导入语句(Import Statement)

import "fmt" 导入了 Go 标准库中的格式化 I/O 包。fmt 包提供了类似 C 语言 printf/scanf 的格式化功能,但更强大和类型安全。

主函数(Main Function)

  • func main() 是程序的入口点,程序从这里开始执行
  • 每个可执行程序必须有且只有一个 main 包,其中包含 main 函数
  • main 函数不接受参数,也不返回任何值

语句执行

fmt.Println("Hello, Go!") 调用 fmt 包的 Println 函数,输出字符串并自动添加换行符。

变量

变量是程序中存储数据的基本单元。Go 语言提供了多种声明变量的方式,每种方式适用于不同的场景。

变量声明的三种方式

方式一:完整声明(显式类型)

var name string = "张三"
var age int = 25
var isStudent bool = true

这种方式最明确,适合需要强调类型的场景。var 关键字声明变量,后面跟着变量名、类型和初始值。

方式二:类型推断

var name = "张三"    // 编译器推断为 string 类型
var age = 25 // 编译器推断为 int 类型
var price = 19.99 // 编译器推断为 float64 类型

Go 编译器会根据右侧的值自动推断变量类型。这种方式代码更简洁,同时保持了类型安全。

方式三:简短声明(仅函数内使用)

name := "张三"      // 等价于 var name = "张三"
age := 25 // 等价于 var age = 25

:= 是 Go 的特色语法,只能在函数内部使用。它同时完成声明和初始化,是 Go 中最常用的变量声明方式。

为什么需要多种声明方式?

  • 完整声明:当你需要显式指定类型,或声明变量但不立即初始化时使用
  • 类型推断:当你希望代码简洁,同时让编译器处理类型时使用
  • 简短声明:在函数内部快速声明临时变量时使用,代码更简洁

多个变量声明

使用 var 块声明多个变量

var (
name string = "张三"
age int = 25
city string = "北京"
isActive bool = true
)

这种形式适合在包级别或函数开头集中声明多个变量,提高代码可读性。

简短声明多个变量

name, age, city := "张三", 25, "北京"

可以在一行中同时声明和初始化多个变量,这是 Go 中常见的写法。

变量命名规则

Go 的变量命名遵循以下规则:

  1. 字符组成:由字母、数字和下划线组成
  2. 首字符:必须以字母或下划线开头,不能以数字开头
  3. 大小写敏感nameName 是不同的变量
  4. 关键字限制:不能使用 Go 的关键字(如 var, func, package 等)

命名约定

  • 使用驼峰命名法(camelCase):userName, maxCount
  • 首字母大写的变量/函数是导出的(public),可被其他包访问
  • 首字母小写的变量/函数是未导出的(private),仅在当前包内可见
  • 使用有意义的名称:totalPricetp 更好

常量

常量是在编译时确定且运行期间不能修改的值。

基本常量声明

const Pi float64 = 3.14159
const MaxConnections = 100
const WelcomeMessage = "欢迎使用本系统"

常量使用 const 关键字声明,可以是任何基本数据类型。

常量组声明

const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)

将相关的常量组织在一起,提高代码可读性。

iota 枚举器

iota 是 Go 提供的常量计数器,从 0 开始,在 const 块中每行自动递增 1:

const (
Red = iota // 0
Green // 1
Blue // 2
)

const (
KB = 1 << (10 * iota) // 1 << 0 = 1
MB // 1 << 10 = 1024
GB // 1 << 20 = 1048576
TB // 1 << 30 = 1073741824
)

iota 特别适合定义枚举类型和位掩码,让代码更清晰且易于维护。

数据类型

Go 是静态类型语言,每个变量都有明确的类型。理解数据类型对于编写正确的程序至关重要。

基本类型概览

Go 提供了丰富的基本数据类型,分为以下几类:

类别类型说明
整数int, int8, int16, int32, int64有符号整数
整数uint, uint8, uint16, uint32, uint64无符号整数
浮点float32, float64浮点数
复数complex64, complex128复数
布尔bool布尔值
字符串string字符串

整数类型详解

有符号整数

var a int8   // -128 ~ 127,占用 1 字节
var b int16 // -32768 ~ 32767,占用 2 字节
var c int32 // -2147483648 ~ 2147483647,占用 4 字节
var d int64 // 极大范围,占用 8 字节
var e int // 平台相关:32位系统为 int32,64位系统为 int64

无符号整数

var f uint8   // 0 ~ 255,常用于表示字节
var g uint16 // 0 ~ 65535
var h uint32 // 0 ~ 4294967295
var i uint64 // 0 ~ 18446744073709551615
var j uint // 平台相关的无符号整数

类型别名

var k byte    // uint8 的别名,用于表示原始字节数据
var l rune // int32 的别名,用于表示 Unicode 码点

如何选择整数类型?

  • 一般情况下使用 int,它足够处理大多数场景
  • 需要明确大小时使用定长类型(如 int32, int64)
  • 处理二进制数据或网络协议时使用 byte
  • 处理 Unicode 字符时使用 rune
  • 需要表示非负数时考虑使用 uint 类型

浮点数类型

var a float32  // 32位浮点数,约 6-7 位有效数字
var b float64 // 64位浮点数,约 15-16 位有效数字(推荐)

// 声明并赋值
price := 19.99 // 默认为 float64
pi := 3.14159265359 // 默认为 float64

浮点数注意事项

  • 默认的浮点数字面量是 float64
  • 金融计算需要精确小数时,应使用 math/big 包中的 Decimal 类型
  • 浮点数比较应该使用容差值,而不是直接比较
// 浮点数比较的正确方式
import "math"

func floatEquals(a, b float64) bool {
return math.Abs(a-b) < 1e-9
}

复数类型

Go 原生支持复数运算,这在科学计算和信号处理中很有用:

var c1 complex64   // 由两个 float32 组成
var c2 complex128 // 由两个 float64 组成(常用)

// 创建复数
c := 3 + 4i
fmt.Println(real(c)) // 3 - 实部
fmt.Println(imag(c)) // 4 - 虚部

// 使用 complex 函数创建
z := complex(3.0, 4.0) // 3 + 4i

布尔类型

var isActive bool = true
var isDeleted bool = false

// 简短声明
isValid := true

布尔运算

a, b := true, false

fmt.Println(a && b) // false - 逻辑与
fmt.Println(a || b) // true - 逻辑或
fmt.Println(!a) // false - 逻辑非

Go 是强类型语言,布尔值不能与其他类型隐式转换:

// 错误:不能将整数转换为布尔值
// if 1 { ... } // 编译错误

// 正确写法
if x != 0 { ... }

字符串类型

字符串是 Go 中重要的内置类型,用于表示文本数据。

字符串声明

// 双引号字符串(可包含转义字符)
str1 := "Hello, Go!\nWelcome!"

// 反引号原始字符串(保留原样,不处理转义)
str2 := `第一行
第二行
第三行`

// 空字符串
var str3 string = ""
str4 := ""

字符串的特点

  • Go 字符串是不可变的字节序列
  • 字符串使用 UTF-8 编码
  • 字符串可以包含任意字节,但通常用于存储文本

字符串操作示例

str := "Hello, Go!"

// 获取长度(字节数,不是字符数)
fmt.Println(len(str)) // 10

// 访问字节
fmt.Println(str[0]) // 72 (H 的 ASCII 码)
fmt.Println(string(str[0])) // H

// 字符串切片
fmt.Println(str[0:5]) // Hello
fmt.Println(str[7:]) // Go!
fmt.Println(str[:5]) // Hello

注意:字符串是不可变的,不能通过索引修改:

str := "Hello"
// str[0] = 'h' // 编译错误:不能修改字符串

指针

指针是 Go 语言中一个重要且独特的概念。理解指针对于编写高效的 Go 程序至关重要,特别是当涉及到函数参数传递、修改数据或避免复制大型数据结构时。

什么是指针?

指针是一个变量,其值是另一个变量的内存地址。打个比方,如果变量是一个存放数据的盒子,那么指针就是一张写着盒子位置的纸条。通过这张纸条,你可以找到盒子并操作里面的数据。

在 Go 中,指针有两个核心操作:

  • &(取地址运算符):获取变量的内存地址
  • *(解引用运算符):通过地址访问存储在该地址的值

指针的声明与初始化

基本语法

var ptr *int    // 声明一个指向 int 类型的指针

此时 ptr 的值为 nil,因为它还没有指向任何变量。

获取变量地址

x := 42
ptr := &x // ptr 现在存储了 x 的内存地址

fmt.Printf("x 的值: %d\n", x) // 42
fmt.Printf("x 的地址: %p\n", &x) // 0xc0000b2008(示例地址)
fmt.Printf("ptr 的值: %p\n", ptr) // 0xc0000b2008(与 &x 相同)
fmt.Printf("ptr 指向的值: %d\n", *ptr) // 42

通过指针修改值

x := 42
ptr := &x

*ptr = 100 // 通过指针修改 x 的值
fmt.Println(x) // 100

指针的零值

指针的零值是 nil,表示指针不指向任何变量:

var ptr *int
fmt.Println(ptr) // <nil>
fmt.Println(ptr == nil) // true

// 解引用 nil 指针会导致 panic
// fmt.Println(*ptr) // panic: runtime error

安全使用指针

func safeDereference(ptr *int) int {
if ptr == nil {
return 0 // 或返回默认值
}
return *ptr
}

指针与函数

理解指针在函数调用中的作用是 Go 编程的关键。

值传递(默认行为)

Go 函数参数默认是值传递,函数内部修改参数不会影响原变量:

func modifyValue(x int) {
x = 100 // 修改的是副本
}

func main() {
num := 42
modifyValue(num)
fmt.Println(num) // 42(原值不变)
}

指针传递

传递指针允许函数修改原始变量:

func modifyPointer(x *int) {
*x = 100 // 通过指针修改原变量
}

func main() {
num := 42
modifyPointer(&num)
fmt.Println(num) // 100(原值被修改)
}

何时使用指针参数

场景推荐原因
需要修改原变量指针值传递无法修改原变量
大型结构体指针避免复制带来的性能开销
可选参数指针nil 可以表示"未提供"
简单值类型复制开销小,代码更简洁

new 函数

Go 提供了 new 函数来创建指针:

ptr := new(int)  // 分配内存,返回 *int
fmt.Println(*ptr) // 0(int 的零值)

*ptr = 42
fmt.Println(*ptr) // 42

new(T)&T{} 的区别:

// 对于结构体
type Person struct {
Name string
}

p1 := new(Person) // 返回 *Person,字段为零值
p2 := &Person{} // 同上
p3 := &Person{Name: "张三"} // 可以同时初始化字段

// 对于基本类型
i1 := new(int) // 返回 *int,值为 0
// i2 := &int(42) // 编译错误:不能对基本类型字面量取地址

x := 42
i2 := &x // 正确:对变量取地址

指针与结构体

指针在结构体操作中非常常见:

type Person struct {
Name string
Age int
}

func (p *Person) SetAge(age int) {
p.Age = age // 自动解引用,等价于 (*p).Age = age
}

func main() {
p := &Person{Name: "张三"}
p.SetAge(25) // 方法调用,Go 自动处理指针

fmt.Printf("%+v\n", p) // &{Name:张三 Age:25}
}

结构体指针的自动解引用

p := &Person{Name: "张三", Age: 25}

// 以下两种写法等价
fmt.Println(p.Name) // 自动解引用
fmt.Println((*p).Name) // 显式解引用

指针与切片/映射

切片和映射本身就是引用类型,通常不需要使用指针:

// 切片:内部已包含指针
func modifySlice(s []int) {
s[0] = 100 // 可以修改底层数组
}

// 映射:内部已包含指针
func modifyMap(m map[string]int) {
m["key"] = 100 // 可以修改原映射
}

func main() {
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // [100 2 3]

m := map[string]int{"key": 1}
modifyMap(m)
fmt.Println(m) // map[key:100]
}

注意:虽然切片和映射可以修改元素,但如果需要修改切片本身(如长度变化),仍需传递指针:

func appendElement(s *[]int, val int) {
*s = append(*s, val) // 可能触发扩容,改变切片头
}

func main() {
s := []int{1, 2, 3}
appendElement(&s, 4)
fmt.Println(s) // [1 2 3 4]
}

Go 指针的安全性

Go 的指针设计比 C/C++ 更安全:

1. 不支持指针运算

arr := [5]int{1, 2, 3, 4, 5}
ptr := &arr[0]

// ptr++ // 编译错误:不支持指针运算
// ptr += 4 // 编译错误

这避免了 C/C++ 中常见的指针越界问题。

2. 垃圾回收

Go 有垃圾回收机制,即使指针指向的内存在函数返回后,只要还有引用存在,就不会被回收:

func createPointer() *int {
x := 42
return &x // 安全:x 的内存不会在函数返回后立即释放
}

3. 空指针检查

解引用空指针会 panic,但不会导致未定义行为:

var ptr *int
// *ptr = 10 // panic: nil pointer dereference

指针的最佳实践

1. 不要过度使用指针

// 不推荐:简单值也用指针
func add(a, b *int) *int {
result := *a + *b
return &result
}

// 推荐:简单值使用值传递
func add(a, b int) int {
return a + b
}

2. 方法接收者的选择

type Counter struct {
value int
}

// 如果需要修改结构体,使用指针接收者
func (c *Counter) Increment() {
c.value++
}

// 如果只是读取,可以使用值接收者
func (c Counter) Value() int {
return c.value
}

// 最佳实践:保持一致性,如果有一个方法用指针,全部都用指针

3. 避免返回局部变量的指针

虽然 Go 允许这样做(逃逸分析会处理),但可能影响性能:

// 可以工作,但可能触发逃逸分析
func getValue() *int {
x := 42
return &x // x 会逃逸到堆上
}

指针与值类型的区别

特性值类型指针类型
赋值复制完整数据复制地址(8字节)
函数传递复制数据复制地址
修改影响不影响原值影响原值
比较运算比较数据内容比较地址
零值类型的零值nil

类型转换

Go 是强类型语言,不同类型之间必须进行显式转换:

// 数值类型转换
var a int = 10
var b float64 = float64(a) // int 转 float64

var c float64 = 3.14
var d int = int(c) // float64 转 int(截断小数部分)

// 字符串和字节切片转换
str := "Hello"
bytes := []byte(str) // 字符串转字节切片
str2 := string(bytes) // 字节切片转字符串

// 字符串和 rune 切片转换
runes := []rune(str) // 字符串转 rune 切片(处理 Unicode)
str3 := string(runes) // rune 切片转字符串

类型转换注意事项

  • 高精度转低精度可能丢失数据
  • 浮点数转整数会截断小数部分(不是四舍五入)
  • 字符串和数字之间不能直接转换,需要使用 strconv 包
import "strconv"

// 字符串转整数
num, err := strconv.Atoi("123") // num = 123

// 整数转字符串
str := strconv.Itoa(123) // str = "123"

// 字符串转浮点数
f, err := strconv.ParseFloat("3.14", 64)

// 浮点数转字符串
s := strconv.FormatFloat(3.14, 'f', 2, 64) // s = "3.14"

零值

在 Go 中,声明但未初始化的变量会被赋予该类型的"零值":

var i int       // 0
var f float64 // 0
var b bool // false
var s string // "" (空字符串)
var arr [5]int // [0 0 0 0 0]
var p *int // nil
var m map[string]int // nil
var slice []int // nil

零值机制确保变量总是有确定的值,避免了未初始化变量带来的不确定性。

运算符

Go 提供了丰富的运算符,用于执行各种计算和操作。

算术运算符

a, b := 10, 3

fmt.Println(a + b) // 13 - 加法
fmt.Println(a - b) // 7 - 减法
fmt.Println(a * b) // 30 - 乘法
fmt.Println(a / b) // 3 - 整除(整数相除结果也是整数)
fmt.Println(a % b) // 1 - 取余(求模)

整数除法的特点

fmt.Println(7 / 2)   // 3(不是 3.5)
fmt.Println(7.0 / 2) // 3.5(浮点数除法)

比较运算符

a, b := 10, 3

fmt.Println(a == b) // false - 等于
fmt.Println(a != b) // true - 不等于
fmt.Println(a > b) // true - 大于
fmt.Println(a < b) // false - 小于
fmt.Println(a >= b) // true - 大于等于
fmt.Println(a <= b) // false - 小于等于

Go 支持链式比较

x := 5
fmt.Println(1 < x && x < 10) // true

逻辑运算符

a, b := true, false

fmt.Println(a && b) // false - 逻辑与(两边都为真才为真)
fmt.Println(a || b) // true - 逻辑或(一边为真即为真)
fmt.Println(!a) // false - 逻辑非(取反)

短路求值

// && 短路:左边为假时,右边不执行
if false && expensiveFunction() {
// expensiveFunction 不会被执行
}

// || 短路:左边为真时,右边不执行
if true || expensiveFunction() {
// expensiveFunction 不会被执行
}

位运算符

位运算符直接操作二进制位,在底层编程和性能优化中很有用:

a, b := 5, 2  // 二进制: 5 = 101, 2 = 010

fmt.Println(a & b) // 0 - 按位与(都为1才为1)
fmt.Println(a | b) // 7 - 按位或(有一个为1即为1)
fmt.Println(a ^ b) // 7 - 按位异或(不同为1)
fmt.Println(a &^ b) // 5 - 位清除(AND NOT)
fmt.Println(a << b) // 20 - 左移(相当于乘以 2^b)
fmt.Println(a >> b) // 1 - 右移(相当于除以 2^b)

位运算应用示例

// 使用位运算设置权限标志
const (
ReadPermission = 1 << 0 // 0001 = 1
WritePermission = 1 << 1 // 0010 = 2
ExecPermission = 1 << 2 // 0100 = 4
)

// 组合权限
permissions := ReadPermission | WritePermission // 0011 = 3

// 检查权限
hasRead := permissions&ReadPermission != 0 // true
hasExec := permissions&ExecPermission != 0 // false

赋值运算符

a := 10

a += 5 // a = a + 5 = 15
a -= 3 // a = a - 3 = 12
a *= 2 // a = a * 2 = 24
a /= 4 // a = a / 4 = 6
a %= 5 // a = a % 5 = 1

// 位运算赋值
a &= 3
a |= 3
a ^= 3
a <<= 2
a >>= 2

字符串操作深入

strings 包常用函数

import "strings"

str := "Hello, Go!"

// 大小写转换
fmt.Println(strings.ToUpper(str)) // HELLO, GO!
fmt.Println(strings.ToLower(str)) // hello, go!

// 包含判断
fmt.Println(strings.Contains(str, "Go")) // true
fmt.Println(strings.HasPrefix(str, "Hello")) // true
fmt.Println(strings.HasSuffix(str, "Go!")) // true

// 查找和替换
fmt.Println(strings.Index(str, "Go")) // 7
fmt.Println(strings.Replace(str, "Go", "World", 1)) // Hello, World!

// 分割和拼接
parts := strings.Split("a,b,c", ",") // ["a", "b", "c"]
fmt.Println(strings.Join(parts, "-")) // a-b-c

高效字符串构建

字符串在 Go 中是不可变的,频繁拼接字符串会产生大量临时对象。对于大量字符串拼接,应使用 strings.Builder

import "strings"

// 方式一:+ 拼接(适合少量字符串)
str := "Hello" + ", " + "Go!"

// 方式二:fmt.Sprintf(适合格式化)
str := fmt.Sprintf("Hello, %s!", "Go")

// 方式三:strings.Builder(适合大量拼接,性能最好)
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(", ")
builder.WriteString("Go!")
result := builder.String() // Hello, Go!

// 方式四:bytes.Buffer(类似 Builder,但功能更多)
import "bytes"
var buffer bytes.Buffer
buffer.WriteString("Hello")
buffer.WriteString(", ")
buffer.WriteString("Go!")
result := buffer.String()

性能对比

  • + 拼接:每次都会产生新字符串,时间复杂度 O(n²)
  • strings.Builder:内部使用可变缓冲区,时间复杂度 O(n)
  • 大量拼接时,Builder 比 + 快数十倍甚至上百倍

输入输出

fmt 包格式化输出

import "fmt"

// 基本输出
fmt.Print("Hello") // 不换行
fmt.Println("Hello") // 换行
fmt.Printf("Hello %s\n", "Go") // 格式化输出

// 格式化占位符
name := "张三"
age := 25
fmt.Printf("姓名: %s, 年龄: %d\n", name, age)

// 输出到字符串
str := fmt.Sprint("Hello") // 返回字符串
str = fmt.Sprintf("Hello %s", "Go") // 格式化返回字符串
str = fmt.Sprintln("Hello") // 返回带换行的字符串

常用格式化动词

动词说明示例
%v默认格式fmt.Printf("%v", 123) // 123
%T类型fmt.Printf("%T", 123) // int
%d十进制整数fmt.Printf("%d", 123) // 123
%b二进制fmt.Printf("%b", 5) // 101
%o八进制fmt.Printf("%o", 8) // 10
%x十六进制(小写)fmt.Printf("%x", 255) // ff
%X十六进制(大写)fmt.Printf("%X", 255) // FF
%f浮点数fmt.Printf("%f", 3.14) // 3.140000
%e科学计数法fmt.Printf("%e", 1234.5) // 1.234500e+03
%s字符串fmt.Printf("%s", "hello") // hello
%q带引号的字符串fmt.Printf("%q", "hello") // "hello"
%p指针fmt.Printf("%p", &x) // 0xc0000...
%t布尔值fmt.Printf("%t", true) // true

格式化修饰符

num := 42

fmt.Printf("|%d|\n", num) // |42|
fmt.Printf("|%5d|\n", num) // | 42| (宽度5,右对齐)
fmt.Printf("|%-5d|\n", num) // |42 | (宽度5,左对齐)
fmt.Printf("|%05d|\n", num) // |00042| (宽度5,前导0)

pi := 3.14159
fmt.Printf("|%.2f|\n", pi) // |3.14| (保留2位小数)
fmt.Printf("|%8.2f|\n", pi) // | 3.14| (宽度8,2位小数)

读取输入

import (
"bufio"
"fmt"
"os"
)

// 读取单个值
var name string
fmt.Print("请输入姓名: ")
fmt.Scan(&name)
fmt.Println("你好,", name)

// 读取多个值
var name string
var age int
fmt.Print("请输入姓名和年龄: ")
fmt.Scan(&name, &age)

// 读取整行(推荐)
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入一行文字: ")
line, _ := reader.ReadString('\n')
fmt.Println("你输入的是:", line)

注释

良好的注释习惯是编写可维护代码的重要部分。

// 单行注释:解释代码的某一行

/*
多行注释
用于较长的说明
或临时禁用代码块
*/

// 文档注释:出现在包、函数、类型声明前,会被 godoc 工具提取
// Add 返回两个整数的和
// 参数 a: 第一个加数
// 参数 b: 第二个加数
// 返回值: 两数之和
func Add(a, b int) int {
return a + b
}

注释规范

  • 使用 // 进行单行注释
  • 包注释应该放在 package 声明前
  • 导出(首字母大写)的函数、类型、变量必须有文档注释
  • 注释应该解释"为什么"而不是"是什么"

小结

本章我们学习了 Go 语言的基础语法:

  1. 程序结构:包声明、导入语句、main 函数
  2. 变量声明:var 声明、类型推断、简短声明
  3. 常量:const 声明、iota 枚举器
  4. 数据类型:整数、浮点数、布尔值、字符串
  5. 类型转换:显式转换规则
  6. 运算符:算术、比较、逻辑、位运算
  7. 字符串操作:strings 包、高效字符串构建
  8. 输入输出:fmt 包格式化
  9. 注释:单行、多行、文档注释

练习

  1. 变量练习:声明不同类型的变量并打印,观察零值
  2. 类型转换:实现摄氏度到华氏度的转换
  3. 字符串操作:编写程序统计字符串中某个字符出现的次数
  4. 位运算:实现一个简单的权限管理系统
  5. 格式化输出:美化打印一个表格

参考资源