文章目录
- 【C++转GO】初阶知识
【C++转GO】初阶知识
注释
和C++一样,使用//或者/**/
前者在vscode快捷键ctrl+/ 后者快捷键shift+alt+a
代码格式
go
package main // 声明文件所在的包,每个go文件必须有所属的包
import "fmt" // 引入的包
func main(){ // 这个{不能在下一行写,为了固定代码风格
fmt.Println("hello") // 末尾不需要; go编译器会自动在这种语句每行结尾加;,所以,如果想一行执行两个语句,需要自己在中间加; 不建议,因为go强制这样风格就是为了简洁,Println()后自动输入换行符
}
编译运行
shell
go build -o a.out test.go # 默认会生成同名称.exe后缀的文件。不过注意先把go.exe路径放在环境变量里
a.out # 运行
# 前面两条指令等价于go run test.go 。这个run会自动编译运行,然后删除.exe文件,因此看起来比a.out直接执行会慢一个编译和删除的速度
gofmt -w test.go # gofmt.exe和go.exe在同一个路径下,可以完成代码格式化的操作
gofmt test.go # 可以完成代码格式化的操作,但是不会写入到指定文件里,只会打印在终端
数据类型
go
package main
import "fmt" // 注意了,如果在后续程序没有用到这个包,就会报错,这是go为了简洁代码
// var n7 = 100
// var n8 = 2
var(
n7 = 100
n8 = 2
)
func main(){
// 在函数内部变量叫法和C++一样,都叫局部变量,建立在外部的变量叫全局变量
var num int = 4 // 指定类型
var num1 = 4 // 自动推导类型为int(这里int和C++int不一样,后面讲数据类型着重讲)
// 注意了,如果创建了变量但是没有使用,会报错。这是go为了简洁代码
fmt.Println(num)
fmt.Println(num1)
num2 := 5 // 使用:可以省略var
var num3 int // 不初始化
fmt.Println(num2)
fmt.Println(num3) // 不初始化默认是0
var n1,n2,n3 int
fmt.Println(n1, n2, n3)
var n4,name,n5 = 10, "jack", 7
fmt.Println(n4, name, n5)
}
基本数据类型
基本数据类型分为:数值型、字符型、布尔型、字符串
数值型
分为整数和浮点数
整数
有符号 int8/16/32/64,无符号 uint8/16/32/64。int/uint 位数随平台(32位机器4B、64位机器8B),uint类似C++的size_t组合:int有符号、uint无符号。go的类型如果超范围会报错,这点和C++不一样!!!
byte仅是uint8的别名,常用于原始字节或ASCII;rune是int32别名,用于Unicode码点。
byte
go
var c1 byte = '('
var c2 byte = '中'
fmt.Println(c1) // 这个打印会打印(的ASCII码
fmt.Println(c1 + 20) // 40
// 字符本身就是整数所以可有那算
fmt.Println(c2) // 这个打印不了,因为汉字三字节。golang默认使用UTF-8编码
// var c2 int = '中'
// fmt.Println(c2) 这样就可以打印
// 如果希望不打印数字,使用格式化打印字符:
fmt.Printf("%c", c1)
//ps: 如果希望打印一大段代码在终端,需要使用`...` 而不是C++的R"(...)"
// 如果希望字符串拼接多行,但是go不允许一行超过80字符,所以要换行相加,这个时候注意在换行时,加号一定作为当前行最末尾,这个时候不会给你加;
浮点数
float32 float64 只有这两种类型,没有double!! 默认推到类型赋值是使用的后者。
字符型
没有单独的类似于char的字符型,golang用byte表示ASCII字节,用rune表示Unicode码点(更像C++的char32_t)。len("汉")返回3(UTF-8字节数),len([]rune("汉"))返回1。range遍历字符串时按rune切分,普通索引用的是字节。
go
package main
import (
"fmt"
"unsafe"
)
func main(){
var c byte = 'A' // C++: char
var rc rune = '啊' // C++: char32_t
fmt.Printf("byte=%c size=%d\n", c, unsafe.Sizeof(c)) // 1B
fmt.Printf("rune=%c size=%d\n", rc, unsafe.Sizeof(rc)) // 4B
}
布尔型
和C++一摸一样,除了打印的时候有点不同。
-
C++printf打印------
printf(true)一定会有问题,因为printf要求传入参数是const char*嘛,如果cout << true 会打印1. 另外如果printf((const char*) true);也一定有问题,因为true本身其实就是1,这样就会打印1地址处的内容 -
go Println打印------
fmtPrintln(true)直接打印出来true了
字符串
这里的字符串也叫string,但是和C++string区别很大。
如果我们希望看到变量的类型是什么,可以使用格式化输出:
go
fmt.Printf("nums的类型是:%T ", nums)
字符串与C++差异
- 不可变 :Go的
string底层是只读[]byte,想改必须转成[]byte或[]rune;C++的std::string可原地改。 - 原始字符串 :Go用反引号
...表示原样字符串;C++用R"(...)。 - 长度语义 :
len(str)返回字节数,len([]rune(str))才是字符数;C++的size()同样是字节 - 切片成本 :
s[a:b]只创建视图,共享底层;C++的substr会拷贝。 - 拼接 :Go用
+或strings.Builder(类似C++的std::ostringstream),频繁拼接用Builder更快。
其他类型转字符型
方法一:使用fmt的Sprintf
go
package main
import "fmt"
func main(){
var n1 int = 19
var n2 float = 1.1
var n3 byte = 'a'
// %T可以打印类型,%q可以打印字符串
var s1 string = fmt.Sprintf("%d",n1)
fmt.Printf("s1对应的类型是:%T ,s1 = %q \n",s1, s1)
var s2 string = fmt.Sprintf("%f",n2)
fmt.Printf("s2对应的类型是:%T ,s2 = %q \n",s2, s2)
var s3 string = fmt.Sprintf("%t",n3)
fmt.Printf("s3对应的类型是:%T ,s3 = %q \n",s3, s3)
var s4 string = fmt.Sprintf("%c",n4)
fmt.Printf("s4对应的类型是:%T ,s4 = %q \n",s4, s4)
}
方式二:使用strconv
go
package main
import(
"fmt"
"strconv"
)
func main(){
var n1 int = 18
var s1 string = strconv.FormatInt(int64(n1),10) //参数:第一个参数必须转为int64类型 ,第二个参数指定字面值的进制形式为十进制
fmt.Printf("s1对应的类型是:%T ,s1 = %q \n",s1, s1)
var n2 float64 = 4.29
var s2 string = strconv.FormatFloat(n2,'f',9,64)
//第二个参数:'f'(-ddd.dddd) 第三个参数:9 保留小数点后面9位 第四个参数:表示这个小数是float64类型
fmt.Printf("s2对应的类型是:%T ,s2 = %q \n",s2, s2)
var n3 bool = true
var s3 string = strconv.FormatBool(n3)
fmt.Printf("s3对应的类型是:%T ,s3 = %q \n",s3, s3)
}
字符型转其他类型
使用conv的Parse方法
go
func ParseInt(s string, base int, bitSize int)(i int64, err error)
func ParseFloat(s string, bitSize int)(i float64, err error)
类型转换
golang是强类型,有比较严格的类型要求
var n1 int = 100var n2 float = n1这个操作是不被允许的,必须需要使用显式类型转换才可以赋值var n2 float = (float)n1var n3 int64 = 100000var n4 int8 = (int8)n3 + 10整形和整形之间也是一样,必须保证等号两边类型一致,而且,如果超出了范围,编译不会出错,但是会数据溢出var n5 int8 = (int8)n3 + 128这样编译是不会通过的,因为等号右边会都被记为int8类型,但是128本身超过了int8范围,所以会报错
指针
如何使用new
go
package main
func main (){
num := new(int) // num 的类型是*int
}
和C++不同的点:
因为Golang是强类型语言:
go
func main(){
var num int = 64
// var ptr *float = &num // error
}
标识符
也就是变量名
要注意不同的点:
- 下划线被称为空标识符,他是一个特殊的标识符,他可以代表任何其他的标识符,对应的值会被忽略,(比如说忽略某个返回值)。所以仅作为占位符使用
- int float都不是保留关键字,可以被作为标识符使用
- 首字母大写表示可以被其他包访问,如果首字母小写就只能在本包中使用
- 我们使用的package表示文件包声明,import导入的包名是从
$GOPATH/src/后面开始计算的,使用/路径分割,如果我们希望在其他包使用到我们指定的包内部的东西,需要配置GOPATH环境变量
运算符
取余小技巧:a % b = a - a / b * b
++ --
只能后置++ --,并且只能作为+=1来单独使用,不能参与到运算中去
Scanln
go
func main(){
var age int
fmt.Println("请输入年龄:")
Scanln(&age) // 使用换行作为分隔符
var name string
var sex string
Scanln("%s %s",&name, &sex) // 使用空格作为分隔符
}
执行流程
if else
- 不同在,可以在if后面跟变量的定义,用;隔开判断条件
- 虽然也可以加上(),但是为了简洁,专门可以省略(),另外需要注意的还是代码风格
go
package main // 声明文件所在包
import "fmt"
func main(){
num := 4
if num == 5{ // 如果写成=还会被提示报错
fmt.Println("5")
} else if num < 0 {
fmt.Println("-1")
} else {
fmt.Println("hello")
}
if count := 3; count < 3 {
fmt.Println("yes")
} else {
fmt.Println("no")
}
}
switch
go
package main // 声明文件所在包
import "fmt"
func main(){
var num int = 10
switch num { // switch可以是常量、变量、有返回值的函数
case 10, 3, 2: // 可以添加多个值
fmt.Println("hello") // 不需要加break
case 9: // case 不能相同
fmt.Println(9)
fallthrough // fallthrough关键字可以穿透一层,这个穿透被较为switch穿透
default:
fmt.Println(1)
}
}
-
switch可以不带表达式,作为if使用
gopackage main import "fmt" func main(){ var num int = 4 switch { case num == 3: //... case num == 4: //... } } -
可以使用:=
gopackage main import "fmt" func main(){ switch num := 4 { case 3: //... case 4: //... } }
for
- 不加()
- 使用起来十分自由
go
for i := 10; i >= 0; i-- { // var i int = 7 不被允许在这里使用
// ...
}
num := 20
for num <= 5 {
num++
}
for ;num <= 100; {
num++
}
for { // 死循环
// ...
}
for range
golang的新玩法
十分舒服的语法糖,在一个"asadasd中国"这样的字符串下,因为其余字符一个字节,汉字每个是3字节,如果按照字节访问,访问到中国字样下会乱码,为此有个for range
go
for i, value := range str{
fmt.Printf("索引:%d, 具体值为:%c", i, value)
}
打印出来结果索引并不是线性递增的,注意了。这是因为有汉字占3字节
label
go
package main
import "fmt"
func main(){
//双重循环:
label2:
for i := 1; i <= 5; i++ {
for j := 2; j <= 4; j++ {
fmt.Printf("i: %v, j: %v \n",i,j)
if i == 2 && j == 2 {
break label2 //结束指定标签对应的循环
}
}
}
fmt.Println("-----ok")
}
除了break , goto continue一样能用
函数
格式
如果返回值类型就一个的话,那么()是可以省略不写的
go
func cal (num1 int, num2 int) (int, int) {
return num1 + num2, num2
}
// sum1, _ := cal(1,2)
和标识符一样,首字母大写才能被其他包使用
函数重载
GO没有函数重载!!!
多参数
这里可比C++爽多了
go
func test (args...int) {
for i := 0; i < len(args); i++ {
fmt.Println(args[i])
}
}
匿名函数
init函数
init函数可以在main函数执行前先执行,去进行初始化
go
import "test"
import "fmt"
var num int = test()
func test() int{
fmt.Println("test");
return 10;
}
func init(){
fmt.Println("hello")
}
func main(){
fmt.Println("main")
}
整体的执行顺序:
- 先执行引入的test包的全局变量初始化,然后执行test包的init
- 然后接着按照全局变量和init执行后面引入的包
- 最后执行下面的全局变量初始化和init,最后执行main
匿名函数
如果我们希望函数只执行一次,就可以使用匿名函数
- 在定义匿名函数的时候就直接调用
- 可以给匿名函数赋值给一个变量,就可以根据变量一直调用函数了
- 匿名函数可以直接访问外界的变量(类似lambda的引用捕捉)
go
package main
import "fmt"
func main(){
func (num1 int, num2 int) int{
return num1 + num2
}(10, 20)
result := func (num1 int, num2 int) int{
return num1 + num2
}(10, 20)
fmt.Println(result)
sub := func (num1 int, num2 int) int{
return num1 - num2
} // 将一个匿名函数赋值给一个变量,这个变量的类型就是函数类型
sub(10, 20)
}
闭包
go
package main // 声明文件所在包
import "fmt"
func getSum() func (num int) int {
sum := 0
return func(num int) int{
sum += num
return sum
}
}
func main(){
sumFunc := getSum()
for i := 0; i < 4; i++ {
fmt.Println(sumFunc(i))
}
}
Go 的闭包 机制会改变变量的存储位置:当内部匿名函数引用了外部函数的局部变量时,Go 编译器会识别这种场景,自动将被引用的变量从栈(stack)转移到堆(heap)上,而非留在栈中。这就从根本上改变了变量的生命周期 ------ 堆上的变量不会随函数栈帧销毁,而是由 Go 的垃圾回收(GC)管理,直到没有任何引用指向它为止。
日期的函数
import "time"
go
package main
import (
"fmt"
"time"
)
func main(){
now := time.Now()
fmt.Printf("now 的类型是:%T\n", now)
fmt.Println("Current time is:", now)
fmt.Println("Year:", now.Year())
fmt.Println("Month:", now.Month())
fmt.Println("Month:", int(now.Month()))
fmt.Println("Day:", now.Day())
fmt.Println("Hour:", now.Hour())
fmt.Println("Minute:", now.Minute())
fmt.Println("Second:", now.Second())
datastr1 := fmt.Sprintf("当前年: %v", now.Year())
fmt.Println(datastr1)
datastr2 := now.Format("2006/01/02 15/04/05")
fmt.Println(datastr2)
datastr3 := now.Format("20062 15:04")
fmt.Println(datastr3)
}
Go 语言的time包为什么能精准识别出你所在地区的本地时间(而非美国等其他时区),核心原因是它不会硬编码任何时区,而是读取你操作系统的时区配置来计算本地时间。
特性
GO的函数自带"包装器"。函数就是数据类型,也可以赋值给变量,可以作为函数参数传参,可以通过变量调用函数。可以通过%T打印函数类型
type
type可以自定义数据类型,类似typedef,不同的是:
go
type myInt int
var num1 myInt = 30
var num2 int = 30
num2 = num1 // 会报错
fmt.Println(num2)
包的引入
go
package main
import "fmt" // 包的引入从&GOPATH/src/后开始计算,使用/路径分离
import "gocode/test/demo2/he" // 导入包的所在目录的路径
func main(){
fmt.Println("hello")
he.Get(); // 方法的第一个字母一定要大写才能被其他包使用
}
-
要注意,一般尽量让目录名和包的名字相同,main函数所在包的名字一定是是main。因为main包是程序的入口包
-
一个目录下同级文件归属一个包,如果设置一个目录下同级文件为不同包,会报错
-
/包的引入从
&GOPATH/src/后开始计算,使用/路径分离 -
需要配置
&GOPATH来配置路径 -
批量导入包和包的别名:
goimport( "fmt" "gocode/cal" // 一定注意了,这是包的所在目录的路径,不是文件路径 test "gocode/testa" ) func main(){ test.Add() }
defer
defer + 语句
能够保障defer后面的语句在当前作用域结束后自动执行
基本语法
go
package main // 声明文件所在包
import "fmt"
func getSum(a int, b int) {
defer fmt.Println("a: ", a)
a += b
fmt.Println("a: ", a)
}
func main(){
getSum(1, 2)
}
注意的是,defer语句机制类似"快照读":到defer语句后,保存当时的值。最后执行defer语句时,访问的是之前快照的值
和recover搭配捕捉错误
go
package main
import (
"fmt"
)
func test() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
num := 10
num2 := 0
fmt.Println(num / num2)
}
func main(){
test()
}
自定义错误类型
go
package main
import (
"fmt"
"errors"
)
func test() error{
num2 := 0
if(num2 == 0){
return errors.New("division by zero")
}
return nil
}
func main(){
test := test()
if test != nil {
fmt.Println("Error:", test)
} else {
fmt.Println("No error")
}
}
数组
go
func main(){
var num [3]int = [3]int{1,2,3}
var arr1 = [3]int{4,5,6}
var num2 = [...]int{7,8,9}
var num3 = [...]int{2:66,0:331,1:77}
fmt.Println(num)
fmt.Println(arr1)
fmt.Println(num2)
fmt.Println(num3)
}
go的数组在传参的时候,如果参数是[]int类型,会进行拷贝。如果是*[]int类型,就可以通过传入指针来避免拷贝
go
func test2(arr *[3]int) {
for i, v := range arr {
fmt.Println(i, v)
}
arr[0] = 100
}
func main(){
var num [3]int = [3]int{1,2,3}
var arr1 = [3]int{4,5,6}
var num2 = [...]int{7,8,9}
var num3 = [...]int{2:66,0:331,1:77}
fmt.Println(num)
fmt.Println(arr1)
fmt.Println(num2)
fmt.Println(num3)
test2(&num)
fmt.Println(num[0])
num4 := [5]int{1,2,3,4,5}
fmt.Println(num4)
}
二维数组的初始化:
go
var arr1 [2][3]int = [2][3]int{{1},{2}} // 没初始化到的,编译器会自动初始化为0
切片
Go通常使用切片来代替数组。切片类似于对数组中的局部进行引用。
内存结构
切片是底层数组的引用类型,包含三个字段:
-
指针(ptr):指向底层数组的某个位置
-
长度(len):当前切片的长度
-
容量(cap):从指针位置到底层数组末尾的长度
需要注意的是,切片切出的片段是左闭右开的,比如:var slice []int = intarr[1:3] 实际上是索引1,2的内容
使用
go
// 共享数组
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // s 的底层数组就是 arr,都在栈上
// 使用make创建
slice := make([]int, 4, 20) // 三个参数:切片类型,切片长度,切片容量
slice2 := []int{1,2,3} // 原理类似make
copy(slice, slice2) // 将slice2的元素内容复制到slice里
copy
-
元素值是深拷贝(底层数组中的值被复制)
-
切片的三个字段(ptr, len, cap)不改变
扩容后的行为(异地扩容)
当切片需要扩容时,Go 会:
- 创建新的底层数组:分配更大的内存空间
- 复制旧数据:将原切片的所有元素复制到新数组
- 更新切片引用:切片指向新数组,与原数组断开联系
go
func example() {
arr := [3]int{1, 2, 3}
slice := arr[:] // 引用整个数组
// 不扩容:修改影响原数组
slice[0] = 99
// arr = [99, 2, 3] ✓
// 扩容后:切片指向新数组
slice = append(slice, 4, 5, 6) // 触发扩容
slice[0] = 100
// arr = [99, 2, 3] (不变)
// slice = [100, 2, 3, 4, 5, 6] (新数组)
fmt.Println(len(slice))
fmt.Println(cap(slice))
}
总结:扩容后切片指向新的底层数组,与原数组完全分离。这是 Go 实现动态数组的方式,既保证了效率(容量足够时不复制),也保证了语义清晰(扩容后与原数组独立)。
要注意的是,切片是动态数组,[3]int,这种数组类型不可以扩容,不可以append
数组和切片的内存分配
Go 编译器在编译时会进行逃逸分析(Escape Analysis) ,决定变量应该分配在栈上还是堆上。这与 C++ 的显式 new/malloc 分配不同。
-
如果数组内存太大,为了防止栈溢出,会选择在堆上开辟空间
-
切片的变量都是在栈上。其实和C++的vector很像,都是同样的三个字段,并且这三个字段都存在栈上。不同的是,切片的指针指向的空间可能在栈上(逃逸分析决定),但是vector指向的空间就是在堆上
-
可能逃逸到堆的情况:
函数返回切片
gofunc getSlice() []int { s := []int{1, 2, 3} // 底层数组逃逸到堆,注意了,这里的[]里为空,这个时候才返回切片,否则返回的是数组 return s // 栈销毁后仍要使用 }动态分配(make/append)
gos := make([]int, 100) // 底层数组在堆上 s = append(s, 1) // 扩容时新数组在堆上切片大小超过一定阈值(编译器决定,通常 >64KB)
-
可能在栈上的情况
从数组创建的切片(共享数组)
goarr := [5]int{1, 2, 3, 4, 5} s := arr[1:3] // s 的底层数组就是 arr,都在栈上小的字面量切片(编译器优化)
gos := []int{1, 2, 3} // 可能栈上(编译器优化)
map
底层哈希表
使用
go
var a map[int]string
a = make(map[int]string, 10) // map可以存放10个键值对,第二个参数可以忽略,默认分配一个内存。不过也可以自动扩容
b := make(map[int]string)
c := map[int]string{
2000 : "lp",
1999 : 'x',
}
清空
go没有提供clear方法,只能通过遍历key来一个个删除,或者使用map = make(...) , make一个新的,让原来的成为垃圾,被GC回收
查找
go
val, err := map[key] // 第二个参数判断是否找到
插入
这里和c++ 不同的一点是,C++只要使用了[]就自动插入了,Go就没有这个顾虑。
面向对象编程
概念
| 特性 | C++ | Go |
|---|---|---|
| 类/结构体 | class / struct |
struct |
| 方法 | 成员函数 | 方法(函数 + 接收者) |
| 继承 | 类继承(单继承/多继承) | ❌ 不支持继承,使用组合嵌入 |
| 接口 | 抽象类/虚函数 | interface(隐式实现) |
| 多态 | 虚函数表(vtable) | 接口实现 |
| 访问控制 | public, private, protected |
首字母大小写(包级别) |
| 构造函数 | 构造函数 | 工厂函数 |
| 析构函数 | 析构函数 | defer + 清理函数 |
结构体
go
type Person struct {
name string // 小写开头:包内可访问。大写开头的话,包外可以访问
age int
}
// Go 没有构造函数,使用工厂函数
func NewPerson(name string, age int) *Person {
return &Person{
name: name,
age: age,
}
}
// 方法
func (p *Person) SetName(name string) {
p.name = name // 实际上是(*p).name,可以直接通过指针和.访问,因为GO对此进行了优化
}
func (p *Person) GetName() string {
return p.name
}
func (p *Person) SetAge(age int) {
p.age = age
}
func (p *Person) GetAge() int {
return p.age
}
func (p *Person) Introduce() {
fmt.Printf("I'm %s, %d years old.\n", p.name, p.age)
}
关键差异:
- C++ 使用
class关键字,Go 使用struct - Go 没有构造函数,通常使用
NewXxx工厂函数 - Go 的方法定义在结构体外部
String函数
如果结构体绑定了String会自动调用
go
package main
import (
"fmt"
)
type Person struct {
name string
}
func (a Person) String() string {
return a.name
}
func main(){
var a *Person = new(Person)
a.name = "John Doe"
fmt.Println(a)
}
方法接收者
go
package main
import (
"fmt"
)
type Person struct {
name string
}
func (a *Person) test1() {
fmt.Println("test1")
}
func (a Person) test2() {
fmt.Println("test2")
}
func main(){
var alice = Person{"alice"}
(&alice).test1()
(&alice).test2() // 虽然使用了指针调用,但test2是值接收者,Go会拷贝一份值来调用
alice.test1() // 虽然使用了值调用,但test1是指针接收者,Go会取地址来调用
alice.test2()
}
初始化
go
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
func main(){
p1 := Person{name: "Alice", age: 30}
fmt.Printf("Name: %s, Age: %d\n", p1.name, p1.age)
p2 := Person{"alice", 30}
fmt.Printf("Name: %s, Age: %d\n", p2.name, p2.age)
p3 := &Person{name: "Bob", age: 25}
fmt.Printf("Name: %s, Age: %d\n", p3.name, p3.age)
}
Go组合嵌入
go
// "基类"结构体
type Animal struct {
name string // 小写:包内可访问
age int
}
func (a *Animal) Eat() {
fmt.Printf("%s is eating.\n", a.name)
}
// "派生类":通过嵌入实现组合
type Dog struct {
Animal // 嵌入,不是继承
breed string
}
func NewDog(name string, age int, breed string) *Dog {
return &Dog{
Animal: Animal{name: name, age: age},
breed: breed,
}
}
// 实现接口(类似多态)
func (d *Dog) Speak() {
fmt.Printf("%s says: Woof!\n", d.name)
}
func (d *Dog) Fetch() {
fmt.Printf("%s is fetching.\n", d.name)
}
// 使用
func example() {
dog := NewDog("Buddy", 3, "Golden Retriever")
dog.Speak() // 调用 Dog 的方法
dog.Eat() // 调用嵌入的 Animal 的方法(自动提升)如果dog实现了Animal的方法会先执行dog实现的方法。总体逻辑和C++继承的逻辑一样,先从子类寻找,如果没有再去父类寻找、然后执行。不只是方法,对于成员也一样。如果希望访问匿名结构体(嵌入的结构体)的成员,可以指明匿名结构体名字来访问
dog.Fetch() // Dog 自己的方法
}
嵌入的特性:
go
type Dog struct {
Animal // 嵌入
}
func main() {
dog := &Dog{Animal: Animal{name: "Buddy", age: 3}}
// 方式1:直接访问嵌入字段
dog.name = "Max" // ✅ 自动提升
// 方式2:通过类型名访问
dog.Animal.name = "Max" // ✅ 也可以
// 方式3:调用嵌入的方法
dog.Eat() // ✅ 自动提升
// 方式4:通过类型名调用
dog.Animal.Eat() // ✅ 也可以
}
接口
定义和实现
go
// 定义接口(隐式实现)
type Drawable interface {
Draw()
}
// 实现接口(无需显式声明)
type Circle struct {
radius float64
}
func (c *Circle) Draw() {
fmt.Printf("Drawing a circle with radius %.2f\n", c.radius)
}
type Rectangle struct {
width, height float64
}
func (r *Rectangle) Draw() {
fmt.Printf("Drawing a rectangle %.2fx%.2f\n", r.width, r.height)
}
// 使用
func drawShapes(shapes []Drawable) {
for _, shape := range shapes {
shape.Draw() // 多态调用
}
}
func main() {
shapes := []Drawable{
&Circle{radius: 5.0},
&Rectangle{width: 10, height: 20},
}
drawShapes(shapes)
}
接口可以组合嵌入接口
go
// 接口组合
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// 组合接口
type ReadWriter interface {
Reader
Writer
}
// 实现 ReadWriter 需要同时实现 Reader 和 Writer
type File struct{}
func (f *File) Read(data []byte) (int, error) {
// ...
}
func (f *File) Write(data []byte) (int, error) {
// ...
}
// File 自动实现了 ReadWriter
空接口 interface{}
go
// 空接口可以表示任何类型(类似 void* 或 std::any)
package main
import (
"fmt"
)
type E interface{}
func main(){
e := 1
var e1 E = e
fmt.Println(e1)
}
多态(借助接口实现)
go
type Shape interface {
Area() float64
Print()
}
type Circle struct {
radius float64
}
func (c *Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func (c *Circle) Print() {
fmt.Printf("Circle with radius %.2f\n", c.radius)
}
type Square struct {
side float64
}
func (s *Square) Area() float64 {
return s.side * s.side
}
func (s *Square) Print() {
fmt.Printf("Square with side %.2f\n", s.side)
}
func demonstratePolymorphism() {
shapes := []Shape{
&Circle{radius: 5.0},
&Square{side: 4.0},
}
for _, shape := range shapes {
shape.Print() // 多态调用
fmt.Printf("Area: %.2f\n", shape.Area())
}
// Go 的 GC 会自动清理,无需手动 delete
}
多态对比:
| C++ | Go |
|---|---|
| 虚函数表(vtable) | 接口表(itable) |
| 需要继承关系 | 只需实现接口方法 |
| 显式继承 | 隐式实现 |
| 编译时确定 | 运行时确定 |
类型断言和类型开关
go
func processShape(s Shape) {
// 类型断言
if circle, ok := s.(*Circle); ok {
fmt.Printf("It's a circle with radius %.2f\n", circle.radius)
}
// 类型开关
switch shape := s.(type) { // 这个type是go的关键字,固定写法
case *Circle:
fmt.Printf("Circle: radius=%.2f\n", shape.radius)
case *Square:
fmt.Printf("Square: side=%.2f\n", shape.side)
default:
fmt.Println("Unknown shape")
}
}