Golang——Go语言基础知识
本文详细介绍Go语言的基础知识,包括数据类型,深浅拷贝,编程范式,Go语言是一种静态(静态类型语言 和 静态语言)强类型、编译型、并发型,并具有垃圾回收功能的编程语言。
文章目录
- 1. Go 语言基础知识
- 数据类型
- 基本数据类型
- 基本数据类型
- 复合数据类型
- 深拷贝与浅拷贝
- 浅拷贝(Shallow Copy)
- 深拷贝(Deep Copy)
- 函数中传递数据
- 值传递(Pass by Value)
- 引用传递(Pass by Reference)
- Go 中都是值传递的优势
- 总结
- 2. Go 语言与编程范式对比
- 编译型语言 vs 解释型语言
- 编译型语言
- 以Go为例
- 解释型语言
- 动态语言 vs 静态语言
- 动态语言
- 静态语言
- 动态类型语言 vs 静态类型语言
- 动态类型语言
- 静态类型语言
- 强类型语言 vs 弱类型语言
- 强类型语言
- 弱类型语言
- 动静态语言与动静态类型的区分
- 3. Go 语言面向对象特性与 Java 对比
- 1. 继承
- Java
- Go
- 2. 封装
- Java
- Go
- 3. 多态
- Java
- Go
- 结论
- GOROOT、GOPATH和 Go Modules
- GOROOT 和 GOPATH 的区别
- Go Modules 引入后的变化
- Go Modules 的变化
- 示例:使用 Go Modules
- 示例结构
1. Go 语言基础知识
下面将详细介绍 Go 语言的理论知识,包括数据类型、深拷贝与浅拷贝、以及如何在函数中传递数据。
数据类型
Go 语言是强类型的,意味着变量在使用时必须明确指定类型。Go 语言有很多内建的数据类型,主要可以分为以下几类:
基本数据类型
- 布尔类型(bool):表示真(
true
)或假(false
)。 - 数字类型:
- 整数:
int
、int8
、int16
、int32
、int64
、uint
、uint8
、uint16
、uint32
、uint64
。 - 浮点数:
float32
、float64
。 - 复数:
complex64
、complex128
。
- 整数:
- 字符类型(rune 和 byte):
rune
是int32
的别名,表示一个 Unicode 字符,通常用于表示多字节字符(如中文字符)。byte
是uint8
的别名,表示单个字节(8 位数据),用于处理 ASCII 字符、字节流、二进制数据等。
- 字符串类型(string):由一系列字符组成,Go 中的字符串是不可变的。
基本数据类型
- 布尔型(bool): 用于表示
true
或false
值。 - 整型(int、int8、int16、int32、int64): 用于表示整数,可以是有符号的整数(
int
)或无符号的整数(uint
)。 - 浮点型(float32、float64): 用于表示带小数的数字。
- 字符型(rune): 用于表示Unicode字符,
rune
实际上是int32
的别名。 - 复数型(complex64、complex128): 用于表示复数,复数由实部和虚部组成。
- 字符串(string): 用于表示文本数据,是一个不可变的字符序列。
复合数据类型
-
非引用类型:
- 数组(Array):Go 中的数组具有固定大小,数组类型由元素类型和数组长度共同决定。例如:
var arr [5]int
。 - 结构体(Struct):Go 中的结构体是一种自定义的数据类型,用于聚合不同类型的多个字段。
- 定义结构体:
type Person struct { Name string Age int }
- 数组(Array):Go 中的数组具有固定大小,数组类型由元素类型和数组长度共同决定。例如:
-
引用类型:
- 指针: 存储变量的内存地址。
- 切片(Slice):切片是 Go 中动态数组的抽象。切片并不包含数组的长度信息,它是一个指向数组的引用,可以动态改变大小。
- 映射(Map):Go 中的映射类似于其他语言中的哈希表(字典),用于存储键值对。可以通过
make
创建一个映射。 - 通道(channel): 是一种用于在 goroutine 之间进行通信的引用类型。它是一种管道,可以让一个 goroutine 发送数据到另一个 goroutine,从而实现数据传递和同步。
- 函数: 函数本身也可以作为类型,传递和使用。
-
接口(Interface):Go 中的接口是一种类型,定义了一组方法,但不需要实现这些方法。任何类型只要实现了接口中的所有方法,就隐式地实现了该接口。
深拷贝与浅拷贝
在 Go 中,“拷贝” 是指将一个变量的值传递到另一个变量。根据拷贝的方式不同,可以分为浅拷贝和深拷贝。
浅拷贝(Shallow Copy)
浅拷贝是指直接复制一个变量的值,如果这个变量是引用类型(如切片、映射、指针等),则复制的是引用地址,即新变量和原变量指向同一内存地址,修改其中一个变量的内容会影响到另一个。
- 对于切片、映射、指针等引用类型,执行浅拷贝时,两个变量会共享底层的数据结构。
示例:
a := []int{1, 2, 3}
b := a // 浅拷贝,b 和 a 指向同一个切片
b[0] = 100
fmt.Println(a[0]) // 输出 100,a 和 b 是共享内存的
深拷贝(Deep Copy)
深拷贝是指创建一个完全独立的新变量,并且复制变量的值以及其所有引用的对象。如果变量是引用类型,深拷贝会复制其底层的数据,而不是引用。
- 需要手动实现深拷贝,尤其是对于复杂的结构体(例如结构体内包含切片、映射等引用类型)。
示例:
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a) // 通过 copy 函数实现深拷贝
b[0] = 100
fmt.Println(a[0]) // 输出 1,a 和 b 是独立的
对于结构体,如果结构体包含引用类型字段,也可以通过 deepcopy
函数实现深拷贝。
函数中传递数据
在 Go 语言中,所有的函数参数传递都是 值传递,也就是说,传递的是参数的副本,即使是传递指针,传递的也是指针的副本。Go中的引用传递是间接实现的。
- Go中的引用传递,通常是指通过传递指针来间接修改原始数据,传递的是指针的副本,而不是原始的内存地址。
- 两个指针的地址是不同的,一个是原始指针的地址,另一个是指针副本的地址。
- 但它们指向的是相同的内存区域,也就是说,指针副本和原始指针都指向同一块数据的内存地址。,因此通过指针可以间接修改原始数据。修改的是 指针指向的内存内容,而不是指针本身。
值传递(Pass by Value)
值传递意味着在函数调用时,将变量的副本传递给函数。如果函数修改了参数的值,它不会影响到原始变量。
示例:
func modify(x int) {
x = 10
}
func main() {
a := 5
modify(a)
fmt.Println(a) // 输出 5,a 的值未被改变
}
引用传递(Pass by Reference)
引用传递是间接实现的,意味着将指针的副本传递给函数。函数可以通过副本指针访问和修改原始变量的值。
传递的是a 的指针的副本p,p的地址和a的地址不一样,但是p的值是a的地址。
示例:
package main
import "fmt"
func modify(p *int) {
// 打印指针 p 的地址:p 变量的内存地址
fmt.Printf("指针 p 的地址:%p\n", &p) // 打印指针变量 p 的内存地址
// 打印指针 p 的值:即指针 p 存储的内存地址(p 的值应该是 a 的地址)
fmt.Printf("指针 p 的值(应该是 a 的地址):%p\n", p) // 打印指针 p 存储的地址(即 p 指向的内存地址)
// 修改指针 p 指向的值
*p = 10
}
func main() {
a := 5
// 打印 a 的值
fmt.Println("传递之前 a 的值:", a)
// 打印 a 的地址:a 变量存储的内存地址
fmt.Printf("a 的地址:%p\n", &a)
// 传递 a 的地址给 modify 函数
modify(&a)
// 打印修改后的 a 的值
fmt.Println("传递之后 a 的值:", a)
}
Go 中都是值传递的优势
Go 中所有函数参数的传递机制都是值传递(pass-by-value),但 Go 提供了指针机制,允许通过指针修改数据。这种机制与很多其他语言(如 C)不同,后者通常会有显式的“引用传递”(pass-by-reference)。
- 简洁性:值传递的方式比较简单,避免了很多指针错误,如悬空指针、内存泄漏等问题。
- 内存安全性:Go 中的垃圾回收机制让指针的使用更加安全,通过值传递避免了很多复杂的内存管理问题。
总结
- 数据类型:Go 提供了多种基本数据类型、复合数据类型(如切片、数组、映射、结构体等)和接口类型。
- 深拷贝与浅拷贝:浅拷贝指的是对引用类型的复制,复制的是引用地址,深拷贝则是完全独立的复制,复制的是数据本身。
- 函数传参:Go 支持值传递和引用传递。值传递会传递数据副本,而引用传递会传递数据的地址(指针),从而允许修改原始数据。
这种设计理念让 Go 语言在并发编程、高效内存管理和简洁易用性方面都表现优异。
2. Go 语言与编程范式对比
Go语言是一种静态(静态类型语言 和 静态语言)强类型、编译型、并发型,并具有垃圾回收功能的编程语言。
编译型语言 vs 解释型语言
编译型语言
- 编译型语言是需要通过编译器将源代码编译成机器码,之后才能执行的语言。编译过程通常包括编译(compile)和链接(linking)两个步骤。
- 编译:把源代码编译成机器码
- 链接:把各个模块的机器码和依赖库串连起来生成可执行文件。
以Go为例
Go语言从源文件到可执行目标文件的转化过程如下:
来源于:Go 程序员面试笔试宝典
这张图描述了 Go 语言 编译系统的整个工作流程,将 Go 源代码 转换为 可执行目标程序 的过程。它分为四个主要阶段:编译、汇编、链接 和生成最终的可执行程序。
- 编译:翻译为汇编代码。
- 汇编:转换为二进制机器码。
- 链接:解决依赖,生成完整的可执行文件。
各模块及其功能:
- 源代码(hello.go):Go 编写的源代码。
- 编译器:将源代码转换为汇编代码(
hello.s
)。 - 汇编器:将汇编代码转换为二进制的目标文件(
hello.o
)。 - 链接器:将目标文件
hello.o
与依赖库(如fmt.o
)进行链接,生成最终的可执行程序(hello
)。
各阶段详细解释如下:
- 源代码阶段(hello.go)
- 输入:Go 源代码文件
hello.go
。 - 文本格式:源代码是可读的文本文件,包含 Go 语言编写的代码。
- 任务:提供程序的逻辑与功能实现。
- 编译器阶段(hello.s)
- 编译器的作用:Go 编译器会将 Go 源代码(
hello.go
)翻译成 汇编程序(hello.s
)。 - 输出:
hello.s
文件,包含汇编代码,是与机器底层指令相关的文本。 - 文本格式:汇编代码仍是人类可读的,但更加接近于底层硬件的操作。
- 汇编器阶段(hello.o)
- 汇编器的作用:将汇编代码(
hello.s
)转换为 二进制机器码,生成目标文件(hello.o
)。 - 输出:
hello.o
文件(目标程序),是 可重定位目标程序,尚未与库进行链接。 - 二进制格式:
hello.o
是不可直接运行的二进制文件,需要进一步链接。
- 链接器阶段(hello 和 fmt.o)
- 链接器的作用:
- 将
hello.o
文件与标准库(如fmt.o
)或其他依赖的目标文件进行链接。 - 解决函数调用和变量引用,将所有二进制代码组合为一个完整的程序。
- 将
- 输入:
hello.o
:主程序的可重定位目标文件。fmt.o
:标准库fmt
的目标文件(用于fmt.Println
等函数)。
- 输出:最终的可执行文件
hello
。 - 二进制格式:生成的
hello
是一个可以直接运行的二进制目标程序。
解释型语言
- 解释型语言的程序不需要预先编译,相比编译型语言省了道工序,解释性语言在运行程序的时候才逐行翻译。
编译型语言包括:C、C++、Delphi、Pascal、Fortran、Go
解释型语言包括:Basic、javascript、python、PHP
说明:
- Java 并不是完全的编译型语言。Java 实际上是 编译-解释型语言,它首先将源代码编译成字节码(.class 文件),然后通过 Java 虚拟机(JVM)解释执行字节码。Java 的编译过程并不像 C/C++ 那样直接生成机器码,所以它不应被归类为纯粹的编译型语言。
- Go 是编译型语言,它直接将源代码编译成机器码,并不依赖于虚拟机等中间层。
- PHP 和 Python 是解释型语言,但有一些现代的优化手段,例如 Python 使用
.pyc
文件和 JIT(即时编译)技术提升性能。
动态语言 vs 静态语言
动态语言
- 动态语言:在运行时可以修改代码结构,如新增函数、修改对象等。
静态语言
- 静态语言:在运行时代码结构不可改变。
动态语言:JavaScript、PHP、Python、Ruby、Erlang
静态语言:Java、C、C++、C#、Objective-C、Go
动态类型语言 vs 静态类型语言
动态类型语言
- 动态类型语言:在运行时进行数据类型检查。
静态类型语言
- 静态类型语言:在编译时确定数据类型。
动态类型语言:Python、Ruby、JavaScript、PHP、Perl
静态类型语言:C、C++、C#、Java、Go、Objective-C、Swift
强类型语言 vs 弱类型语言
强类型语言
- 强类型语言:类型严格,变量的数据类型一旦确定就不能更改,不能随便进行隐式类型转换。
弱类型语言
- 弱类型语言:类型较为宽松,可以进行隐式类型转换。
强类型语言:Java、C#、Python、Objective-C、Ruby、Go、C、C++(注意:C 和 C++ 在某些情况下表现为弱类型,但总体上它们是强类型语言)
弱类型语言:JavaScript、PHP
说明:
- C 和 C++ 确实可以进行隐式类型转换(如将字符赋值给整型变量),这使得它们在某些情况下表现得像 弱类型语言。但整体上它们仍然是 强类型语言,因为在大多数情况下,它们要求显式地进行类型转换,且类型检查比较严格。
动静态语言与动静态类型的区分
动静态语言:是指运行时代码结构是否可以改动
动静态类型:是检查数据类型的时机是在运行时还是运行前
强弱类型:是指数据类型是否可以改变。
3. Go 语言面向对象特性与 Java 对比
1. 继承
Java
Java 继承是通过父类和子类的关系来实现,子类通过
extends
关键字继承父类后,子类拥有父类所有非private
的属性和方法。
区别(重载 vs 重写):
- 重载(Overloading):是方法名相同,但参数不同(包括参数类型、个数、顺序)。这发生在同一个类中。
- 重写(Overriding):是子类重新定义父类的某个方法,方法签名(名称、参数类型、返回值类型)必须相同。
Go
Go 中没有显式的继承机制,但可以通过 组合(embedding)来实现类似继承的功能。在 Go 中,
struct
可以嵌入其他struct
,嵌入的struct
的字段和方法可以直接访问(interface
也可以继承,但大多数情况下,interface
用做多态)。
示例代码:
package main
import "fmt"
type People struct {
Name string
Age int
}
type Student struct {
People // 结构体 People 的嵌入
Score int
}
func main() {
var lcc Student
lcc.People.Age = 18
lcc.Name = "lichuachua"
lcc.Score = 100
fmt.Println(lcc) // {{lichuachua 18} 100}
}
2. 封装
Java
Java 通过
public
、private
和protected
控制对类的成员的访问权限,实现封装。
Go
Go 通过首字母是否大写来判断访问权限:
- 首字母大写为 public,可以被其他包访问。
- 首字母小写为 private,它是私有的,只能在同一包内访问。
3. 多态
Java
Java 的多态通过继承、重写和父类引用指向子类对象实现。
- 继承(或接口实现):使得子类继承或实现父类/接口的行为。
- 方法重写:允许子类提供特定的行为,重写父类的行为。
- 父类引用指向子类对象:通过父类或接口引用指向子类对象,动态调用子类的方法,从而实现多态性。
Go
Go通过接口(interface) 来实现多态,结构体实现接口的所有方法,就可以认为它实现了该接口。这使得 Go 的多态更加灵活和松耦合。
Go 中没有类的概念,只有接口(interface
)和结构体(struct
)。一个结构体只要实现了接口中的所有方法,就可以被认为实现了该接口,这与 Java 的implements
关键字不同。
示例代码:
package main
import "fmt"
type Animals interface {
Say()
}
type Dog struct{}
type Cat struct{}
func (d Dog) Say() {
fmt.Println("wangwang")
}
func (c Cat) Say() {
fmt.Println("miaomiao")
}
func main() {
var d Dog
d.Say() // 输出:wangwang
var c Cat
c.Say() // 输出:miaomiao
// 使用接口变量,可以接受任何实现了 Say() 方法的类型
var a Animals
a = d
a.Say() // 输出:wangwang
a = c
a.Say() // 输出:miaomiao
}
说明:
- 通过接口
Animals
和实现了Say()
方法的Dog
和Cat
类型,Go 实现了多态的特性。在main
函数中,接口类型的变量a
可以存储任何实现了Say()
方法的类型(如Dog
或Cat
)。
结论
- 继承:Go 使用组合(embedding)来实现类似继承的效果,Java 使用类继承。Go 中没有直接的继承机制。
- 封装:Java 使用访问修饰符控制访问权限,Go 使用首字母大小写来控制访问权限。
- 多态:Java 通过继承、重写和父类引用来实现多态,Go 通过接口(interface)来实现多态,Go 的多态更加灵活,没有显式的
implements
关键字。
GOROOT、GOPATH和 Go Modules
GOROOT
:Go 的安装目录,通常不需要修改。GOPATH
:Go 项目的工作目录,存放源代码、依赖和编译结果。在 Go 1.11 以后,Go Modules 的引入使得GOPATH
变得不再是强制要求,开发者可以自由选择工作目录。- Go Modules:在 Go 1.11 以后成为默认的依赖管理方式,可以绕过
GOPATH
,直接在任何目录下进行开发。
GOROOT 和 GOPATH 的区别
属性 | GOROOT | GOPATH |
---|---|---|
作用 | 指定 Go 的安装目录,包括 Go 编译器和标准库。 | 指定 Go 项目的工作目录,包含源代码、依赖包、可执行文件等。 |
目录结构 | 包含 Go 编译器、标准库、工具等。 | 包含项目代码、第三方包、编译结果等。 |
设置方式 | 通常由 Go 安装自动配置。 | 由用户设置并用于存放代码和依赖。 |
修改权限 | 通常不需要修改。 | 需要开发者根据项目需要设置并修改。 |
目录位置 | 默认安装在 /usr/local/go (Linux/Mac) 或 C:\Go (Windows)。 | 默认是 $HOME/go (Linux/Mac) 或 %USERPROFILE%\go (Windows)。 |
包含内容 | Go 的核心工具和标准库。 | Go 项目的源代码、第三方库、编译后的可执行文件等。 |
Go Modules 引入后的变化
自 Go 1.11 引入 Go Modules 后,GOPATH
的使用发生了一些变化。Go Modules 可以让你在任何目录下工作,而不再需要将项目放在 GOPATH
内。Go Modules 改变了 Go 的包管理方式,使得 GOPATH
的作用逐渐减小,Go 开发者可以脱离 GOPATH
目录来管理项目。
Go Modules 的变化
- 不再依赖于 GOPATH:你可以将代码存放在任何地方,而不必在
GOPATH/src
中。 - 项目级依赖管理:使用
go mod
来管理项目的依赖关系。
Go 1.16 后,Go Modules 成为了默认的依赖管理方式,GOPATH
变得不再那么重要,新的 Go 项目几乎都推荐使用 Go Modules。
示例:使用 Go Modules
在 Go Modules 模式下,你可以在任何目录下初始化项目并开始使用:
go mod init <module-name> # 初始化 Go Modules
go mod tidy # 下载并整理依赖
示例结构
假设你有一个基本的 Go 项目,使用 Go Modules 时,项目结构可能如下:
myproject/
├── go.mod # Go Modules 文件,记录模块的依赖
├── main.go # Go 源代码文件
└── go.sum # Go Modules 校验文件
在 Go 1.11 之前,你的项目需要像这样放在 GOPATH/src
下:
GOPATH/
└── src/
└── myproject/
└── main.go
但随着 Go Modules 的引入,这种要求不再存在,你可以将项目放在任意目录中,只需使用 go mod init
来初始化项目。