【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/64int/uint 位数随平台(32位机器4B、64位机器8B),uint类似C++的size_t组合:int有符号、uint无符号。go的类型如果超范围会报错,这点和C++不一样!!!

byte仅是uint8的别名,常用于原始字节或ASCII;runeint32别名,用于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 = 100
  • var n2 float = n1 这个操作是不被允许的,必须需要使用显式类型转换才可以赋值
  • var n2 float = (float)n1
  • var n3 int64 = 100000
  • var 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使用

    go 复制代码
    package main
    import "fmt"
    
    func main(){
        var num int = 4
        switch {
        case num == 3:
            //...
        case num == 4:
            //...
        }
    }
  • 可以使用:=

    go 复制代码
    package 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来配置路径

  • 批量导入包和包的别名:

    go 复制代码
    import(
        "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 会:

  1. 创建新的底层数组:分配更大的内存空间
  2. 复制旧数据:将原切片的所有元素复制到新数组
  3. 更新切片引用:切片指向新数组,与原数组断开联系
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指向的空间就是在堆上

  • 可能逃逸到堆的情况:

    函数返回切片

    go 复制代码
    func getSlice() []int {
        s := []int{1, 2, 3}  // 底层数组逃逸到堆,注意了,这里的[]里为空,这个时候才返回切片,否则返回的是数组
        return s  // 栈销毁后仍要使用
    }

    动态分配(make/append)

    go 复制代码
    s := make([]int, 100)     // 底层数组在堆上
    s = append(s, 1)          // 扩容时新数组在堆上

    切片大小超过一定阈值(编译器决定,通常 >64KB)

  • 可能在栈上的情况

    从数组创建的切片(共享数组)

    go 复制代码
    arr := [5]int{1, 2, 3, 4, 5}
    s := arr[1:3]  // s 的底层数组就是 arr,都在栈上

    小的字面量切片(编译器优化)

    go 复制代码
    s := []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")
    }
}

相关推荐
小笔学长2 小时前
Mixin 模式:灵活组合对象功能
开发语言·javascript·项目实战·前端开发·mixin模式
IT艺术家-rookie2 小时前
golang--解决 Go 并发场景下的数据竞争问题的方案
golang
我是人机不吃鸭梨2 小时前
Flutter 桌面端开发终极指南(2025版):构建跨平台企业级应用的完整解决方案
开发语言·javascript·人工智能·flutter·架构
夏幻灵2 小时前
[从零开始学JAVA|第一篇 ] 分清关键字 方法名 字面量 标识符
java·开发语言
小徐Chao努力2 小时前
【Langchain4j-Java AI开发】03-提示词与模板
java·开发语言·人工智能
cike_y2 小时前
Spring5入门&IOC容器
java·开发语言·spring·jdk·ioc·jdk1.8
你疯了抱抱我2 小时前
【QQ】空间说说批量删除脚本(不用任何额外插件,打开F12控制台即可使用)
开发语言·前端·javascript
Tandy12356_2 小时前
手写TCP/IP协议栈——实现ping响应不可达
c语言·网络·c++·网络协议·tcp/ip·计算机网络
沐知全栈开发2 小时前
Web 词汇表
开发语言