在Go语言中,程序就像搭积木一样,是由许多小的部分逐步组合起来的。我们用变量来保存数据,用加法、减法等运算把简单的表达式拼成更复杂的表达式。 像整数、浮点数这样的基本类型,可以被组合成数组、结构体等更大的数据结构。
表达式通常出现在语句中,而语句的执行顺序可以通过if、for等控制流程的语句来安排。 我们把相关的语句组织成函数,这样既方便复用,也让代码更清晰。最后,函数被放在不同的源文件和包里,形成一个完整的Go程序。

在Go语言中,给各种程序元素(如函数、变量、常量、类型、语句标签和包)起名字时,需要遵循一套明确的命名规则。 这些规则不仅有助于代码的规范性和可读性,还能避免命名冲突和潜在的错误。比如,函数名和变量名应该简洁明了,能够准确表达其用途;常量通常使用大写字母和下划线分隔单词;
类型名一般采用驼峰命名法,首字母大写以便导出;包名建议使用简短的全小写单词,避免使用下划线或混合大小写。此外,Go语言还规定名称必须以字母或下划线开头, 后面可以跟字母、数字或下划线,并且区分大小写。遵循这些命名约定,可以让团队协作更加顺畅,也让代码更容易维护和理解。
Go语言的命名规则其实很简单:名称必须以字母或下划线开头,后面可以跟任意数量的字母、数字和下划线。
这里的字母不仅包括英文字母,还包括Unicode标准中认定的各种语言的字母,比如中文、日文等。
需要注意的是,Go语言是区分大小写的,所以userName和username是完全不同的两个名称。
Go语言有25个关键字,这些是语言本身保留的特殊词汇,只能在语法允许的地方使用,不能作为我们自定义的名称。这些关键字包括:
除了关键字,Go语言还预定义了一些常用的名称,大约有三四十个,用于内置的常量、类型和函数。这些预声明名称包括:
常量: true、false、iota、nil
类型: int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr、float32、float64、complex128、complex64、bool、byte、rune、string、error
函数: make、len、cap、new、append、copy、close、delete、complex、real、imag、panic、recover
这些预声明名称虽然可以重新定义,但这样做容易造成混淆,所以除非有特殊需要,一般不建议这样做。
在Go语言中,名称的可见性与其声明位置密切相关。如果我们在函数内部声明一个变量,那么这个变量只在该函数内可见,这就是局部变量。 如果我们在函数外部声明,那么这个名称对整个包内的所有文件都是可见的,这就是包级别变量。
更重要的是,名称首字母的大小写决定了它是否可以被其他包访问。如果名称以大写字母开头,那么它是"导出的",意味着其他包可以访问它。 如果以小写字母开头,那么它只能在当前包内使用。这就像我们给房间贴标签一样,大写字母的标签从外面也能看到,小写字母的标签只有房间里面的人才能看到。
Go语言社区形成了一些命名惯例,这些惯例虽然不是强制性的,但遵循它们可以让代码更易读、更专业。
长度选择: 虽然名称长度没有限制,但Go程序员倾向于使用简洁的名称,特别是对于作用域较小的局部变量。
比如循环计数器通常用i、j、k,而不是loopIndex、counter这样的长名称。但对于作用域较大的名称,比如包级别变量或函数名,通常会使用更具描述性的名称。
驼峰命名法: Go程序员在组合多个单词时使用"驼峰命名法",即除了第一个单词外,其他单词的首字母大写。
比如userName、getUserInfo、calculateTotalPrice。这种命名方式比使用下划线分隔更受欢迎,所以你会看到parseRequestLine而不是parse_request_line。
缩写处理: 对于缩写词,比如HTML、JSON、URL等,Go语言的惯例是保持所有字母相同的大小写。所以函数名可能是htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml。
下面是一个实际的命名例子:
|// 包级别变量,可以被其他包访问 var MaxRetryCount = 3 // 包级别变量,只能在当前包内使用 var defaultTimeout = 30 // 函数名,首字母大写表示可以被其他包访问 func CalculateUserScore(userID string) int { // 局部变量,使用简洁名称 score := 0 // 循环计数器使用简短名称 for i := 0; i < len(userID); i++ { if isValidChar
用我们自己的话来说,Go语言里的“声明”其实就是给程序里的各种东西起名字,并且告诉编译器它们是什么类型、有什么用。
比如你要用一个变量、常量、类型或者函数,都得先声明一下。Go里常见的声明方式有四种:用var声明变量,const声明常量,type声明类型,func声明函数。
我们来看个简单的例子,体会一下声明到底是怎么回事。下面这个小程序里就声明了一个常量、一个函数和几个变量:
|// 学生成绩计算程序 package main import "fmt" const maxScore = 100.0 func main() { var score = maxScore var percentage = (score / maxScore) * 100 fmt.Printf("学生得分 = %g分,百分比 = %g%%\n", score, percentage) }
|学生得分 = 100分,百分比 = 100%
函数的声明一般包括:函数名、参数列表(就是调用时要传进去的值)、可选的返回值列表,以及函数体(也就是具体要做什么的代码)。
如果函数没有返回值,返回值列表可以省略。函数从第一个语句开始执行,遇到 return 或到结尾就结束,并把控制权和结果(如果有的话)交还给调用它的地方。
比如下面的例子,写了一个汇率转换的函数,把转换的公式封装起来,这样需要用到的时候直接调用就行,不用每次都写一遍公式:
|// 货币转换程序 package main import "fmt" func main() { const usdRate, eurRate = 6.5, 7.2 fmt.Printf("%g美元 = %g人民币\n", usdRate, usdToCny(usdRate)) fmt.Printf("%g欧元 = %g人民币\n"
|6.5美元 = 42.25人民币 7.2欧元 = 51.84人民币
在Go语言中,变量就像我们生活中的储物盒一样,可以存放各种类型的数据。
当我们使用var关键字声明一个变量时,我们实际上是在告诉计算机:“请给我一个储物盒,它的名字是什么,它能存放什么类型的东西,里面现在放的是什么”。

变量声明的基本格式是这样的:
|var name type = expression
这个格式中,name是变量的名字,type是变量的类型,expression是初始值。类型和初始值都可以省略,但不能同时省略。
如果省略类型,计算机会根据初始值自动推断类型;如果省略初始值,变量会使用该类型的"零值"作为初始值。
Go语言有一个很好的特性叫做"零值机制"。每种类型都有自己的零值:数字类型的零值是0,布尔类型的零值是false, 字符串的零值是空字符串"",而指针、切片、映射等引用类型的零值是nil。 这就像新买的储物盒,里面总是干干净净的,不会有什么乱七八糟的东西。
这种零值机制的好处是,在Go语言中从来不会有"未初始化"的变量。每个变量都有一个明确的值,这让我们写代码时更安全,也更容易处理各种边界情况。比如:
|var name string fmt.Println(name) // ""
这段代码会打印空字符串,而不会出现错误或者不可预测的行为。 Go程序员经常会让复杂类型的零值也有意义,这样变量从一开始就处于有用的状态。
我们可以在一个声明中同时声明多个变量,甚至可以声明不同类型的变量:
|var age, height, weight int // 三个整数变量 var isStudent, gpa, name = true, 3.8, "张三" // 布尔值、浮点数、字符串
变量的初始化可以是字面值,也可以是任何表达式。包级别的变量在程序开始运行之前就初始化好了,而函数内部的局部变量只有在执行到声明语句时才会初始化。
我们还可以通过调用返回多个值的函数来初始化一组变量:
|var file, err = os.Open("data.txt") // os.Open返回一个文件和一个错误
在函数内部,Go语言提供了一种更简洁的变量声明方式,叫做"短变量声明"。
它使用:=符号,格式是name := expression。这种方式让变量的类型由初始值自动推断,就像我们买东西时,售货员会根据我们买的东西自动判断应该用什么袋子装。
短变量声明特别适合在函数内部使用,因为它既简洁又灵活。比如:
|name := "张三" // 字符串类型 age := 25 // 整数类型 isStudent := true // 布尔类型
由于短变量声明的便利性,Go程序员在函数内部声明局部变量时,通常会优先使用这种方式。
而var声明则更多地用于需要明确指定类型的情况,或者当变量稍后会被赋值而初始值不重要的情况。
|score := 95 // 自动推断为int类型 var temperature float64 = 25.5 // 明确指定为float64类型 var students []string // 声明一个字符串切片 var err error // 声明一个错误变量 var student Student // 声明一个Student类型的变量
和var声明一样,我们也可以在同一个短变量声明中同时声明多个变量:
|x, y := 10, 20
不过,当有多个变量需要初始化时,建议只在有助于提高代码可读性的情况下使用,比如在for循环的初始化部分。
这里有一个重要的区别需要记住::=是声明,而=是赋值。多变量声明和元组赋值是不同的概念。在元组赋值中,我们只是给已经存在的变量赋新值:
|x, y = y, x // 交换x和y的值
短变量声明也可以用于调用返回多个值的函数,比如:
|file, err := os.Open("config.txt") if err != nil { return err } // ...使用file... file.Close()
这里有一个微妙但非常重要的细节需要注意:当你使用短变量声明(:=)时,左侧的每一个变量不一定都是“新声明”的变量。如果左侧的某些变量已经在当前的词法块(也就是当前的大括号范围内)被声明过了, 那么短变量声明对于这些变量来说,其实只是一次普通的赋值操作,而不是重新声明。只有那些在当前词法块中还没有出现过的变量,才会被真正声明出来。
举个例子,假设你已经在同一个函数体内声明了变量err,然后你又写了如下代码:
|file, err := os.Open("input.txt") // ... file, err := os.Create("output.txt") // 编译错误:没有新变量
要修复这个问题,我们需要使用普通的赋值语句:
|file, err := os.Open("input.txt") // ... file, err = os.Create("output.txt") // 使用赋值而不是声明
在Go语言中,指针就像我们生活中的门牌号一样,它不直接包含数据,而是告诉我们数据存放在哪里。变量是存储数据的容器,而指针则是这个容器的"地址标签"。
当我们声明一个变量时,比如var x int,这个变量在内存中有一个特定的位置。我们可以通过&x来获取这个变量的地址,这就像问"x住在哪里"一样。&x的结果是一个指针,类型是*int,读作"指向int的指针"。
如果我们把这个指针赋值给另一个变量p,那么p就"指向"了x。我们可以通过*p来访问p指向的变量的值,这就像通过门牌号找到房子一样。
|age := 25 pointer := &age // pointer指向age的地址 fmt.Println(*pointer) // "25" *pointer = 30 // 通过指针修改age的值 fmt.Println(age) // "30"
在这个例子中,pointer存储了age的地址,*pointer让我们能够读取和修改age的值,而不需要直接使用变量名age。
指针的一个重要特性是,任何类型的指针的零值都是nil。我们可以通过比较指针和nil来判断指针是否指向某个变量:
|var a, b int fmt.Println(&a == &a, &a == &b, &a == nil) // "true false false"
在Go语言中,函数返回局部变量的地址是完全安全的。这里的“局部变量”指的是在函数内部用:=或var声明的变量。与C/C++等语言不同,在Go中,即使函数执行完毕,这些变量的内存也不会被立即回收。
原因在于Go的垃圾回收机制会自动检测到这些变量的地址被返回或在外部被引用,于是会将它们“逃逸”到堆上分配,保证它们在外部依然有效。
举个例子,下面的代码中,createValue函数内部声明了一个局部变量value,然后返回了它的地址。每次调用createValue,都会新分配一块内存用于存储value,并返回其指针。
Go会自动处理内存分配和释放,无需程序员手动干预,也不会出现悬垂指针的问题:
|var ptr = createValue() func createValue() *int { value := 42 return &value }
即使这两个指针指向的变量的值相同(比如都是42),但它们在内存中的位置是完全独立的。因此,比较两次调用createValue()返回的指针,会发现它们并不相等,因为它们指向的是不同的内存地址:
|fmt.Println(createValue() == createValue()) // "false"
指针的一个重要作用是让函数能够修改调用者传递的变量。比如下面这个函数,它通过指针来增加变量的值:
|func increment(ptr *int) int { *ptr++ // 增加ptr指向的变量的值 return *ptr } count := 10 increment(&count) // count现在是11 fmt.Println(increment(&count)) // "12"(count现在是12)
指针让我们能够创建变量的"别名",也就是说,多个指针可以指向同一个变量。这就像多个门牌号可以指向同一栋房子一样。这种特性既有好处也有风险:好处是我们可以通过不同的方式访问同一个变量,风险是如果管理不当,可能会让代码变得难以理解。
指针在Go标准库中有很多应用,比如flag包就大量使用指针来处理命令行参数。下面是一个简单的例子,展示了如何使用指针来创建命令行工具:
|// 学生信息处理示例 package main import ( "flag" "fmt" "strings" ) var verbose = flag.Bool("v", false, "显示详细信息") var separator = flag.String("s", "|", "分隔符")
在这个程序中,flag.Bool和flag.String函数返回指向布尔值和字符串的指针。这些指针指向由flag包管理的变量,当我们调用flag.Parse()时,这些变量的值会根据命令行参数进行更新。
指针是Go语言中一个强大而重要的概念,它让我们能够更灵活地操作数据,但同时也需要我们更加小心地管理内存和变量的生命周期。
创建变量的另一种方法是使用内置函数new。表达式new(T)创建一个类型为T的未命名变量,将其初始化为T的零值,并返回其地址,这是类型为*T的值。
|ptr := new(int) // ptr,类型为*int,指向一个未命名的int变量 fmt.Println(*ptr) // "0" *ptr = 100 // 将未命名的int设置为100 fmt.Println(*ptr) // "100"
用new创建的变量和通过取地址符&获得的普通局部变量在本质上没有区别:它们都分配了内存并返回该内存的指针。 不同之处在于,使用new(T)时,无需为变量命名和声明一个临时变量,而是直接获得一个指向类型T零值的指针,这样可以让代码更简洁。 例如,表达式new(int)会分配一个int类型的内存空间,并返回其指针,等价于先声明一个int变量再取其地址。
需要注意的是,new其实只是提供了一种语法上的便利,并不是Go语言中必须掌握的核心概念。下面的两个createInt函数,虽然写法不同,但它们的行为完全一致,都会返回一个指向int类型变量的指针:
|func createInt() *int { return new(int) } func createInt() *int { var temp int return &temp }
每次调用new都返回一个具有唯一地址的不同变量:
|ptr1 := new(int) ptr2 := new(int) fmt.Println(ptr1 == ptr2) // "false"
new函数相对较少使用,因为最常见的未命名变量是结构体类型,对于结构体类型,结构体字面语法更灵活。
变量的生命周期指的是变量在内存中从被分配到被释放的整个时间段。不同类型的变量,其生命周期也有所不同。
包级别变量(即在函数外部声明的变量),在程序启动时就会被分配内存,并且会一直存在直到整个程序退出为止。也就是说,包级变量的生命周期覆盖了程序的整个运行过程,无论在何时何地都可以访问到它们。
局部变量(即在函数内部声明的变量),它们的生命周期则更加动态。每当程序执行到声明语句时,都会为该变量分配新的内存空间。这个变量会一直存在,直到它变得不可达(即在程序的后续执行中再也没有办法访问到它),这时垃圾回收器就有可能回收它所占用的内存。需要注意的是,局部变量的生命周期不仅仅局限于它所在的代码块,有时候如果它的地址被外部变量引用(比如通过指针传递到函数外部),它的生命周期就会被延长,直到最后一个引用它的地方也不可达为止。
函数的参数和返回值其实也是局部变量。每次调用函数时,都会为参数和返回值分配新的内存空间,这些变量的生命周期从函数被调用时开始,到函数返回时结束(除非它们的地址被外部引用,从而逃逸到堆上)。
总之,变量的生命周期由它们的作用域、可达性以及是否被外部引用等因素共同决定。理解变量的生命周期对于编写高效、内存安全的Go程序非常重要。
|// 示例变量定义 var count int = 10 var canvas interface{} var color string = "red" for i := 0; i < count; i++ { x := calculateX(i) y := calculateY(i) drawPoint(canvas, int(x), int(y), color) }
上述例子中,变量i在每次for循环开始时创建,新变量x和y在循环的每次迭代时创建。
垃圾收集器如何知道变量的存储可以被回收?完整的故事比我们这里需要的要详细得多,但基本思想是每个包级别变量和每个当前活动函数的每个局部变量都可能成为通向问题变量的路径的起点或根,沿着指针和其他最终通向变量的引用。 如果不存在这样的路径,变量已经变得不可达,所以它不能再影响计算的其余部分。 因为变量的生命周期只由它是否可达决定,局部变量可能比其封闭循环的单个迭代活得更久。即使在封闭函数返回后,它也可能继续存在。
编译器可能选择在堆或栈上分配局部变量,但也许令人惊讶的是,这个选择不是由使用var还是new声明变量决定的。
|var globalPtr *int func allocateOnStack() { var x int x = 1 globalPtr = &x } func allocateOnHeap() { y := new(int) *y = 1 globalPtr = y }
这里,x必须堆分配,因为它在allocateOnStack返回后仍可从变量globalPtr访问,尽管被声明为局部变量;我们说x从allocateOnStack逃逸。 相反,当allocateOnHeap返回时,变量y变得不可达,可以被回收。由于y不从allocateOnHeap逃逸,编译器安全地在栈上分配*y,即使它是用new分配的。 无论如何,逃逸的概念不是你为了编写正确代码而需要担心的事情,尽管在性能优化期间记住它很好,因为每个逃逸的变量都需要额外的内存分配。
垃圾收集在编写正确程序方面是一个巨大的帮助,但它并没有免除你思考内存的负担。 你不需要显式分配和释放内存,但要编写高效的程序,你仍然需要了解变量的生命周期。 例如,在长寿命对象内保持对短寿命对象的不必要指针,特别是全局变量,将阻止垃圾收集器回收短寿命对象。

在Go语言中,赋值操作就像是把新的物品放进储物盒,替换掉原来的内容。赋值语句的基本格式是:变量名在等号(=)左边,新的值或表达式在等号右边。
例如,x = 5 表示把数字5放进变量x中。赋值不仅可以用于基本类型(如int、float、string等),还可以用于结构体、数组、切片、映射、指针等各种类型。
赋值时,Go会先计算等号右边的表达式,然后把结果存储到左边的变量中。如果左边是一个普通变量,就是直接替换原有的值;如果左边是指针、结构体字段、数组或切片的元素,则会把新值写入对应的内存位置。
需要注意的是,赋值时变量的类型必须兼容,否则编译器会报错。例如,不能把字符串直接赋值给int类型的变量。对于复合类型(如结构体、数组),赋值会把整个值复制一份,而不是只复制引用(切片、映射和通道除外,它们赋值时只复制引用)。
|// 示例变量定义 var score int var ptr *bool var student struct{ name string } var grades []int var index int var factor float64 = 1.5 score = 95 // 给变量score赋值95 *ptr = true // 通过指针给变量赋值 student.name = "李四" // 给结构体字段赋值 grades[index]
Go语言还提供了一些简化的赋值运算符,让我们可以更简洁地写代码。每个算术运算符和位运算符都有一个对应的赋值运算符。比如,我们可以把上面的最后一个语句写成:
|// 示例变量定义 var grades []int var index int var factor float64 = 1.5 grades[index] *= factor
这样写的好处是避免了重复写变量名,代码更简洁,也避免了重复计算表达式的开销。
对于数值变量,我们还可以使用++和--运算符来快速增加或减少变量的值:
|counter := 10 counter++ // 相当于counter = counter + 1,counter变成11 counter-- // 相当于counter = counter - 1,counter又变成10
Go语言还提供了一种更高级的赋值方式,叫做“元组赋值”。所谓元组赋值,就是可以在一条语句中同时给多个变量赋值,语法格式如下:
|// 示例变量定义 var a, b int var students []string var i, j int a, b = b, a students[i], students[j] = students[j], students[i]
这种写法比传统的临时变量方式更简洁,也更不容易出错。
元组赋值在算法实现中特别有用。比如计算两个整数的最大公约数时:
|func gcd(a, b int) int { for b != 0 { a, b = b, a%b } return a }
或者计算斐波那契数列时:
|func fibonacci(n int) int { prev, curr := 0, 1 for i := 0; i < n; i++ { prev, curr = curr, prev+curr } return prev }
元组赋值也可以让一系列简单的赋值变得更紧凑:
|// 示例变量定义 var x, y, z int x, y, z = 10, 20, 30
不过需要注意的是,如果表达式比较复杂,建议避免使用元组赋值,因为分开的语句更容易阅读和理解。
当函数返回多个值时,元组赋值也特别有用。比如:
|file, err = os.Open("data.txt")
这种情况下,左边必须有和函数返回值数量相同的变量。
|file, err = os.Open("config.txt") // 函数调用返回两个值
通常,函数使用这些额外结果来指示某种错误,要么像os.Open调用中那样返回错误,要么返回bool,通常称为ok。 与变量声明一样,我们可以将不需要的值赋给空白标识符:
|// 示例变量定义 var destination, source interface{} _, err = io.Copy(destination, source) // 丢弃字节计数 _, ok = data.(string) // 检查类型但丢弃结果
赋值语句是最直接、显式地将一个值赋给变量的方式,但实际上,在Go程序中还有许多场景会发生“隐式赋值”。下面详细介绍几种常见的隐式赋值情况:
函数调用参数传递:当你调用一个函数时,实参的值会自动赋给函数定义中的形参变量。例如:
|func add(a int, b int) int { return a + b } sum := add(3, 5) // 这里3和5分别隐式赋值给a和b
return语句:当函数有具名返回值时,return语句会把返回的表达式结果隐式赋值给这些返回变量。例如:
|func getName() (name string) { name = "Go"
总之,除了显式的赋值语句外,Go语言在函数调用、返回、复合类型初始化等多种场景下都会自动进行隐式赋值操作。这些隐式赋值遵循与显式赋值相同的类型兼容性规则。
在Go语言中,类型就像我们生活中的分类标签一样,它告诉我们某个数据是什么性质的东西,能做什么操作,以及如何存储。 变量的类型决定了它可能具有的值的特征,比如它占用多少内存空间、在计算机内部如何表示、支持哪些操作,以及有哪些相关的方法。
在实际编程中,我们经常会遇到这样的情况:不同的变量虽然内部表示相同,但代表的概念完全不同。比如,int类型既可以用来表示循环的计数器,也可以表示时间戳、文件描述符或者月份;
float64既可以表示速度(米每秒),也可以表示温度;string既可以表示密码,也可以表示颜色名称。
为了避免这种混淆,Go语言提供了“类型声明”功能,让我们可以创建新的命名类型。这些新类型虽然基于现有的基础类型,但它们是独立的类型,不能随意混用。
类型声明的基本格式是:
|type name underlying-type
类型声明通常出现在包级别,这样新类型在整个包中都可以使用。如果类型名以大写字母开头,那么其他包也可以访问这个类型。
让我们通过一个学生成绩包的例子来说明类型声明的作用:
|// 学生成绩包 package student import "fmt" type Score float64 type Grade string const ( MinScore Score = 0 MaxScore Score = 100 PassScore Score = 60 ) func ScoreToGrade(s Score) Grade { if
在这个例子中,我们定义了两个新类型:Score(分数)和Grade(等级)。虽然它们都基于float64和string类型,但它们是不同的类型,不能直接进行运算或比较。这种区分让我们避免了把分数和等级混在一起的错误。
如果我们需要在两种类型之间转换,必须使用显式的类型转换,比如Score(t)或Grade(t)。这些转换不会改变数据的值,只是改变了类型标签,让计算机知道我们想要的是什么单位。
Go语言支持多种类型转换。如果两个类型有相同的基础类型,或者都是指向相同基础类型的指针,那么它们之间可以相互转换。数值类型之间也可以转换,但可能会改变值的表示。比如把浮点数转换为整数会丢掉小数部分。
命名类型继承了基础类型的所有操作。这意味着Score和Grade类型支持所有float64和string的运算:
|fmt.Printf("%g\n", MaxScore-MinScore) // "100" 分 excellentGrade := ScoreToGrade(MaxScore) fmt.Printf("%g\n", excellentGrade-ScoreToGrade(PassScore)) // 编译错误:字符串不能相减 fmt.Printf("%g\n", excellentGrade-PassScore) // 编译错误:类型不匹配
比较运算符也可以用于命名类型,但只能比较相同类型的值:
|var s Score var g Grade fmt.Println(s == 0) // "true" fmt.Println(s >= 0) // "true" fmt.Println(s == g) // 编译错误:类型不匹配 fmt.Println(s == Score(85)) // "false"!
命名类型还有一个重要特性:我们可以为它们定义特殊的行为,这些行为叫做"方法"。比如,我们可以为Score类型定义一个String方法,控制它如何被打印出来:
|func (s Score) String() string { return fmt.Sprintf("%g分", s) } 当我们使用`fmt`包打印`Score`类型的值时,会自动调用这个`String`方法: ```go s := GradeToScore("优秀") fmt.Println(s.String()) // "95分" fmt.Printf("%v\n", s) // "95分" fmt.Printf("%s\n", s) // "95分" fmt.Println(s) // "95分" fmt.Printf("%g\n", s) // "95" fmt.Println(float64(s)) // "95"
类型声明是Go语言中一个强大的功能,它让我们能够创建更安全、更清晰的代码,避免类型混淆的错误。

包(Package)是Go语言中代码组织和复用的基本单位。包可以理解为一个文件夹,里面包含了一个或多个Go源文件,这些文件共享同一个包名。
包不仅帮助我们组织代码结构,还提供了命名空间隔离、代码复用和模块化开发的能力。每个Go程序都是由一个或多个包组成的,其中main包是特殊的,它定义了程序的入口点。
Go语言中的包具有以下特点:
|// math包中的函数 package math func Add(a, b int) int { return a + b } func multiply(a, b int) int { // 小写开头,包外不可访问 return a * b }
每个Go源文件的第一行都必须是package声明,用于指定该文件属于哪个包。package声明的语法为:
|package main import "fmt" func main() { fmt.Println("Hello, Go!") }
包的结构示例:
|myproject/ ├── main.go // package main ├── utils/ │ ├── helper.go // package utils │ └── validator.go // package utils └── models/ ├── user.go // package models └── product.go // package models
在Go语言中,使用import语句可以将其他包引入到当前文件中,从而使用这些包中定义的函数、类型和变量。Go的包导入方式非常灵活,主要包括以下几种:
init函数,不导入任何标识符,常用于注册驱动等场景。下面分别介绍这些导入方式的具体用法:
|// 标准导入 import "fmt" import "math" // 分组导入(推荐) import ( "fmt" "math" "strings" ) // 别名导入 import ( "fmt" m "math" "strings" ) // 点导入(不推荐,容易造成命名冲突)
Go语言通过标识符的首字母大小写来控制可见性:
|package utils // 导出的函数,其他包可以访问 func PublicFunction() string { return "public" } // 私有函数,只能在当前包内访问 func privateFunction() string { return "private" } // 导出的变量 var PublicVar = "public variable" // 私有变量 var privateVar = "private variable"
当一个包第一次被导入时,Go会自动对其进行初始化。这个过程主要包括三个步骤:首先,包内所有的包级变量会按照声明顺序进行初始化;接着,包中定义的所有init()函数会被依次调用(如果有多个init()函数,会按照它们在源码中的出现顺序执行);最后,如果当前包依赖其他包,Go会确保先初始化依赖的包,再初始化当前包。整个初始化过程只会在包被首次导入时执行一次,确保包的状态在使用前已经准备就绪。
|package mypackage import "fmt" // 包级别变量 var globalVar = initGlobalVar() func initGlobalVar() string { fmt.Println("初始化全局变量") return "global" } // init函数,包导入时自动执行 func init() { fmt.Println("包初始化") } // 可以有多个init函数
Go通过模块系统来管理包的依赖关系。每个Go项目的根目录下通常会有一个go.mod文件,用于声明当前项目是一个模块,并详细列出该模块所依赖的其他模块及其版本信息。go.mod文件不仅指定了项目的模块路径(通常是代码仓库的地址),还通过require语句明确标注了所有依赖包的名称和版本。这样,Go工具链可以自动解析和下载这些依赖,确保项目在不同环境下都能获得一致的依赖包版本。此外,go.sum文件会记录每个依赖包的校验和,用于保证依赖的完整性和安全性。通过这些机制,Go实现了高效、可追溯且易于维护的依赖管理流程。
|// go.mod 文件示例 module myproject go 1.21 require ( github.com/gin-gonic/gin v1.9.1 github.com/go-sql-driver/mysql v1.7.1 )
简单来说,作用域就是变量、函数等名字在代码中能用的范围。比如你在函数里用var x = 1,那x只能在这个函数里用,外面用不了。
再比如在for循环或if语句里声明的变量,也只能在各自的小范围里用。不要把作用域和生命周期搞混了:作用域是“代码里能不能访问到”,生命周期是“变量在程序运行时活多久”。
Go语言中的块作用域(block scope)是由花括号{}包围的代码区域决定的。只要在一对花括号内声明的变量(比如var、:=、const等),它们的名字就只在这对花括号包围的范围内有效,出了这个范围就无法访问。
常见的块作用域有:函数体、if语句、for循环、switch语句、甚至是你手动加的一对花括号。块作用域的好处是可以让变量只在需要的地方可见,避免和外部变量冲突,也让代码更容易理解和维护。
|func main() { x := 1 { y := 2 fmt.Println(x, y) // 可以访问外层变量x和当前块变量y } // fmt.Println(y) // 编译错误:y未定义 }
包作用域指的是:在包级别(即在所有函数、类型、变量声明的最外层)声明的变量、函数、类型,它们在同一个包的所有源文件中都可以被访问和使用。 只要在同一个包里,无论是在同一个文件还是不同文件,这些名字都是可见的。但包外部如果要访问这些名字,必须是以大写字母开头(即导出)的标识符才行。 包作用域常用于定义全局变量、工具函数、类型等,让包内所有代码都能共享和复用。
|package main import "fmt" var globalVar = "全局变量" func globalFunc() { fmt.Println("全局函数") } func main() { fmt.Println(globalVar) // 可以访问包级别变量 globalFunc() // 可以调用包级别函数 }
在Go语言中,导入的包名(比如fmt、math等)只在当前源文件的作用域内有效。也就是说,你在某个文件的import语句中导入了一个包,这个包的名字只能在这个文件里使用,不能在同一个包的其他文件里直接用,
除非在那些文件里也显式导入了同样的包。这种设计可以让每个文件只依赖自己需要的包,避免不必要的依赖污染。例如:
a.go文件里import "fmt",那么fmt只能在a.go里用;b.go文件里没导入fmt,那在b.go里用fmt.Println会报错,必须在b.go里也写上import "fmt"。这种作用域规则让包的依赖关系更加清晰和可控。
|package main import ( "fmt" "math" ) func main() { fmt.Println("Hello") // 可以使用fmt fmt.Println(math.Pi) // 可以使用math } // 在另一个文件中,需要重新导入才能使用这些包
函数参数的作用域是整个函数体,也就是说,所有在函数定义时声明的参数(如name string、age int等),都可以在该函数的任意位置被访问和使用。无论是在函数的开头、中间还是结尾,这些参数始终有效。需要注意的是,如果在函数体内部使用短变量声明(:=)重新声明了与参数同名的变量,那么在该作用域内,新的变量会“遮蔽”原有的参数,直到该作用域结束为止。这样可以临时修改参数的值,但不会影响原始参数在其他地方的可见性。
|func processData(name string, age int) { fmt.Println(name, age) // 参数在整个函数内可用 // 可以重新声明同名变量 name := "新名字" fmt.Println(name) // "新名字" }
在 Go 语言中,for、if、switch 等语句的初始化语句中声明的变量,其作用域仅限于对应的代码块内部。也就是说,这些变量只能在各自的语句体或分支内访问,出了该语句块就无法再使用。例如:
for 循环的初始化部分声明的变量(如 for i := 0; i < 3; i++ 中的 i),只在整个 for 循环体内有效,循环结束后就无法访问。if 语句的初始化部分声明的变量(如 if x := getValue(); x > 0 中的 x),只在该 if 语句的所有分支(包括 else)内有效,出了 if 语句块就无法访问。switch 语句的初始化部分声明的变量(如 switch y := getValue(); ... 中的 y),只在整个 switch 语句的所有分支内有效,出了 switch 语句块就无法访问。这种作用域规则可以避免变量名冲突,使变量的生命周期更加清晰和可控。
|func main() { // for循环的初始化语句中声明的变量 for i := 0; i < 3; i++ { fmt.Println(i) // i在循环体内可用 } // fmt.Println(i) // 编译错误:i未定义 // if语句中的变量 if x := getValue(); x > 0 { fmt.Println("正数:", x) // x在if块内可用 } // fmt.Println(x) // 编译错误:x未定义
8. 变量声明练习
编写一个程序,声明不同类型的变量并打印它们的值。
|package main import "fmt" func main() { // 方法1:使用var声明变量 var age int = 25 var height float64 = 175.5 var name string = "张三" var isStudent bool = true // 方法2:使用短变量声明(:=) age2 := 25 height2 :=
9. 类型声明和常量练习
创建一个表示温度的类型,并定义相关常量。
|package main import "fmt" // 定义Temperature类型(基于float64) type Temperature float64 // 定义常量 const ( FreezingPoint Temperature = 0.0 // 冰点 BoilingPoint Temperature = 100.0 // 沸点 ) // 摄氏度转华氏度 func CelsiusToFahrenheit(c Temperature) Temperature { return c*
10. 作用域练习
编写一个程序,演示不同作用域的变量声明和使用。
|package main import "fmt" // 全局变量(包级别) var globalVar = "全局变量" func main() { // 函数级别变量 localVar := "局部变量" fmt.Println("在函数内可以访问:") fmt.Println(" 全局变量:", globalVar) fmt.Println(" 局部变量:", localVar) // 块作用域
复合字面量赋值:当你用字面量初始化复合类型(如切片、数组、结构体、映射等)时,Go会自动把右侧的每个元素值赋给左侧对应的元素。例如:
|grades := []string{"优秀", "良好", "及格"}
这实际上等价于:
|grades[0] = "优秀" grades[1] = "良好" grades[2] = "及格"
map和channel元素赋值:当你向map或channel中存放元素时,Go也会自动进行赋值操作。例如:
|m := make(map[string]int) m["score"] = 100 // 隐式地将100赋值给m["score"]
输出结果:
|=== 使用var声明 === 年龄: 25 身高: 175.5 姓名: 张三 是否学生: true === 使用短变量声明 === 年龄: 25 身高: 175.5 姓名: 张三 是否学生: true
说明:
var关键字声明变量,可以指定类型:=短变量声明,Go会自动推断类型%d用于格式化整数,%f用于格式化浮点数%s用于格式化字符串,%t用于格式化布尔值输出结果:
|冰点: 0.00°C 沸点: 100.00°C 25.0°C = 77.0°F 体温: 37.50°C
说明:
type关键字定义类型别名const关键字定义常量(t Temperature)表示这是Temperature类型的方法输出结果:
|在函数内可以访问: 全局变量: 全局变量 局部变量: 局部变量 在块内可以访问: 全局变量: 全局变量 局部变量: 局部变量 块变量: 块变量 循环作用域: 循环变量 i = 0 循环变量 i = 1 循环变量 i = 2 在testFunction中: 全局变量: 全局变量 函数变量: 函数变量
说明: