前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从 C++ 到 Go

从 C++ 到 Go

作者头像
韩伟
发布2022-08-26 17:56:18
8200
发布2022-08-26 17:56:18
举报
文章被收录于专栏:韩伟的专栏韩伟的专栏

模块管理

  1. 可执行程序都声明为 "package main",而不是像 C++ 那样找 main() 函数所在源码
  2. 声明为其他 "package xxx" 的代码,会自动编译成  xxx.a,自动打包成静态库比较方便

这是针对 C++ 复杂的 3L(Load, Link, Library) 机制的一个重大修正。在实际开发中,我也比较喜欢把所有的依赖模块都先打包成静态库,然后最终静态链接成一个“几乎没有任何依赖的”可执行文件。而不喜欢通过动态链接依赖操作系统中安装的各种库,因为环境变化太多很容易出问题。

语法差异

  1. Go 会自动在行尾加分号,所以习惯性末尾不加分号,如果要加也可以
  2. Go 的所有声明,都是类型放在名字之后,大部分是三段式:声明关键字 + 名字 + 声明类型
    1. 声明关键字有:var/func/type/const
    2. 声明类型有:int/string/.../struct
代码语言:javascript
复制
var number int // 声明变量 + 名字 number + 整数类型func SomeFun() int { ... }  // 声明函数 + 名字 SomeFun + 返回值类型 + 函数内容type SomeStruct struct { ... } // 声明类型 + 名字 SomeStruct + 结构体类型 + 结构体内容
  1. Go 函数可以返回多个返回值,可以直接对声明中的返回值变量赋值进行返回,也可以用 return 语句加逗号 , 进行多个值返回
代码语言:javascript
复制
func main() {    var min, max int    // 调用    min, max = MinMax(78, 65)    fmt.Printf("Minmium is: %d, Maximum is: %d\n", min, max)}
// 声明返回值变量为 min, max,直接运行过程中对其赋值即可func MinMax(a int, b int) (min int, max int) {    if a < b {        min = a        max = b    } else { // a = b or a > b        min = b        max = a    }    return}
// 没有声明返回值的变量名,通过逗号来 return 多个值func ReturnTwo() (int, int) {    return 2, 3}
  1. Go 的赋值符号是  = 而声明加赋值为 :=
代码语言:javascript
复制
代码语言:javascript
复制
var s = "go"  // 用 "go" 作为值,定义了变量 s 是字符串类型j := 9        // 直接用 9 作为值,定义了变量 j 是数字类型,节省了 var 这个写法

5. Go 语言的类型系统和 C++ 很像,也有基本类型和派生类型,其中派生类型可以组合多个基本类型来构成。Go 的派生类型有:

代码语言:javascript
复制
指针 *type数组 [...]type集合 map[type]type切片 []typeChannel chan type结构体 type XXX struct {...}函数 type XXX func(xxx) xxx {...}接口 type XXX interface {...}Go 语言的中括号 [] 用在了数组、切片、集合三种类型上,比 C++ 仅仅用于数组丰富的多。
  1. 数字类型

无符号

有符号

浮点

数字类型

uint8

int8

float32

byte

uint16

int16

float64

rune

uint32

int32

complex64

uint

uint64

int64

complex128

uintptr

  1. Map 中判断 key 是否存在:
代码语言:javascript
复制
value, ok := m[key] //返回值 2 表示是否存在if (ok) {  ...}
  1. iota 在 const关键字出现时将被重置为 0 (const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。(用来作为错误码似乎不错)
代码语言:javascript
复制
const(    a = iota //0    b  //1    c  //2)
  1. for 循环中,遍历容器用 range
代码语言:javascript
复制
for i, v := range arr {    fmt.Printf("arr[%d]: %v", i, v)}
  1. 回调函数
代码语言:javascript
复制
// 为回调函数定义一个类型,方便作为参数类型type cb func(int) int
// 输入回调函数作为参数的函数func testCallBack(x int, f cb) {    f(x)}
// 具体的回调函数func callBack(x int) int {    return x}
func main() {    // 使用回调    testCallBack(1, callBack)}

内存管理

  1. 有指针类型(操作符和 C 语言一模一样)
    1. 取地址操作符 &
    2. 声明和解引用操作符 *
    3. 使用 . 操作符读取指针指向对象的成员,而不需要 -> 操作符

Go 语言全是值传递,所以必须要有指针类型,否则所有变量都必须要复制值,太浪费性能了。

  1. 函数外定义的为“全局变量”
  2. 如何在堆上申请对象:

    Go 语言通过自动检测“逃逸”来自动决定是否分配在堆上,这样连 new 这种关键字也不需要了,也无需好像 Java 语言一样区分在基本数据类型(在栈上)和对象数据类型(在堆上),所以也无所谓装箱拆箱。也不需要类似 C# 的 struct 类型(值传递的栈上结构)

    1. 可以返回一个局部变量的地址,go 语言会检查到这种情况,而自动把这个局部变量放在堆上,同时也会作为垃圾回收管理起来
    代码语言:javascript
    复制
    func test_ptr() *int {    var i = 10    return &i // go 很聪明的发现了这个情况}
    func main() {    var ret *int    ret = test_ptr()
        fmt.Printf("ret(%x): %v\n", ret, *ret)    //输出:ret(c0000ba000): 10}
    1. 也可以通过输出参数,把一个局部变量的地址传递到函数之外,go 语言也会检查到这种情况,从而把此局部变量放在堆上,以垃圾回收管理
    代码语言:javascript
    复制
    代码语言:javascript
    复制
    type Con struct {    ptr *int}
    func test(in *Con) {    i := 110    in.ptr = &i // 局部变量的值被赋值到函数外了}
    func main() {    var con Con    test(&con)    fmt.Printf("result: %d in %#v\n", *con.ptr, con) // 可以输出 110 这个值
    代码语言:javascript
    复制
    甚至可以把局部变量的指针通过输出参数(指针的指针)传出函数,这样局部变量也可以被放到堆上
    代码语言:javascript
    复制
    func test(in **int) { // 输出参数,一个指针的指针,用来返回一个对象的地址    i := 110    *in = &i}
    func main() {    var result *int // 这个指针对象仅仅用来存放地址    test(&result) // 从 test() 函数中输出可用的对象地址    fmt.Printf("result: %d (%x)\n", *result, result) // 打印出 110 这个值}
  3. 数组的长度是固定的 var variable_name [SIZE] variable_type
代码语言:javascript
复制
balance := [...]float32{1000.0,2.0,3.4.7.2,50.8} // 初始化数组同时定义长度
  1. 数组作为参数时,可以指定长度,也可以不指定长度 arr []int,如果下标访问越界,会抛出错误:panic: runtime error: index out of range
  2. 切片:动态数组,或者内置的队列模板,会动态扩容,但不会动态缩容。cap 参数用于减少扩容的次数,提高性能。
    • append(s1, s2...) 把切片 s2 中的所有元素都添加到 s1 去,对切片扩容主要靠这个手段。
    1. 使用数组进行初始化切片,切片在扩容之前,是和数组共用数据内存空间,修改切片的元素值同时会修改其初始化数组的元素!
    代码语言:javascript
    复制
    代码语言:javascript
    复制
    var slice1 []int = make([]int, 3, 5) //用 make() 来构建一个切片,len=3, cap=5
    s := [] int {1,2,3} //直接初始化切片, [] 表示是切片,cap=len=3arr:=[...] int {1,2,3} // 注意 [...] 表示是数组,而不是切片
    //初始化切片 s,是数组 arr 的引用,用 startIndex/endIndex 进行范围指定s := arr[startIndex:endIndex]
    代码语言:javascript
    复制
    make([]int, len, cap) 返回构造的切片(也可以构造 map)
    1. len(s) 返回长度
    2. cap(s) 返回容量
    3. append(s, x, y, z) 追加 x, y, z 到 s 中去,可以增加 s 的容量 len 或者 cap
    4. copy(s1, s2) 把 s2 的内容拷贝到 s1,控制一个切片中的数据主要靠这个手段,类似于 memcpy(),注意 copy() 并不会扩容,目标切片放不下的数据,会直接丢弃
  • new() 函数返回一个变量的指针,同时也分配这个变量的内存,这个变量的值会全部初始化为 0 或者 nil。这个操作可以视为先声明一个变量,然后再取此变量的指针作为返回值。但是对于符合类型来说,如 slice/map/channel 这些,new() 就无法正确的给予初始化,所以需要 make() 来进行构建。

虽然 make()/new() 看起来很像 C++ 的 new 的作用,但实际并不一定会在堆上生成对象。Go 会自己进行“逃逸分析”来决定,所以好像用不用这两个函数都无所得的——如果不小心在不需要的地方被视为“逃逸”会影响内存分配的性能。

面向对象

  1. 没有“类”的关键字,只要是命名类型或者结构体类型都可以作为类的模板使用。但是 go 依然不是“原型链”方式的“基于对象”方式工作的。还是属于“对象模板”方式,只不过这个模板的关键字往往叫 struct,也是通过建立新的类型(type)来实现的,所以也可以具备静态检查结构的能力。
  2. 对象的方法声明和函数类似,只是在函数名之前增加类型即可。这和 C++ 在 .cpp 中定义方法的代码很像,而且省略了定义类的过程(往往在 .h 中定义)。
代码语言:javascript
复制
// circle.h class Circle {  float radius;  float getArea(); // golang 不需要专门声明方法}
// circle.cpp#include "circle.h"float Circle::getArea() {  // golang 只需要定义具体的方法即可  return 3.14 * radius * radius;}
  1. 没有 self 或者 this 关键字,而是在声明方法时自己定义
代码语言:javascript
复制
// 定义“类”type Circle struct {  radius float64}
// 定义方法,c 变量就是 thisfunc (c Circle) getArea() float64 {  return 3.14 * c.radius * c.radius}
func main() {  var c1 Circle // 声明对象  c1.radius = 10.00 // 初始化对象  fmt.Println("面积 = ", c1.getArea()) // 调用对象的方法}
  1. 需要特别注意的是,调用方法的时候,对象是值传递的。所以如果想修改对象的内容,声明方法的时候,对于 this 对象,必须要用指针类型!
代码语言:javascript
复制
// 对象变量声明是指针 *Circle  func (c *Circle) changeRadius(radius float64) {  c.radius = radius}
  1. 访问控制修饰符:Go 语言没有 public/protected/private 这些关键字,而是通过名字的大小写来控制,属于一种“隐喻”:
    1. 大写字母开头的 == public
    2. 非大写字母开头的 == private

    这么一来,代码规范中的 Camel 方式和 PASCAL 方式的整理不存在了,首字母带了功能,不是随便能改的了。

    很多 Go 程序的私有成员变量,都用 _ 开头,这和 google 的代码规范有一定的关系。C++ 的 google 代码规范规定:私有成员变量以下划线 _ 结尾。

  2. 继承:在 struct 中定义的匿名类型成员 (可以是 int/float/string,或者各种 struct),就是父类;还可以多重继承!但是如果包含了多个相同类型的匿名类型成员,则只视为一个匿名类型成员。
代码语言:javascript
复制
type Pet struct {  name string}type Dog struct {  Pet  // 继承 Pet 类型  Breed string}
// Pet 的 Speck() 方法func (p *Pet) Speak() string {  return fmt.Sprintf("my name is %v", p.name)}
// Pet 的 Name() 方法func (p *Pet) Name() string {  return p.name}
// Dog 的 Speak() 方法func (d *Dog) Speak() string {  return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed)}func main() {  d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"}  fmt.Println(d.Name())  // 调用基类 Pet 的 Name()  fmt.Println(d.Speak())  // 调用子类 Dog 的 Speak()}
  1. 接口 interface 是一批方法的集合
    1. 定义接口
    代码语言:javascript
    复制
    type Phone interface {    call()}
    1. 实现接口,并不需要显式的标明 interface 的名字
    代码语言:javascript
    复制
    type NokiaPhone struct {}func (p *NokiaPhone) call() {  fmt.Println("I'm Nokia, I can call you!")}
    1. 调用接口的方法,注意:接口变量是一个指针类型!
    代码语言:javascript
    复制
    func main() {  var phone Phone // 这是一个指针类型  phone = new(NokiaPhone) // new() 返回的是 *NokiaPhone  phone.call()}

错误处理

  • Go 语言利用多返回值来返回错误,总体思路还是和 C++ 返回错误码的处理类似
代码语言:javascript
复制
func dosomething() (int, error) {    return 1, nil}
func main() {    value, err := dosomething();    // 立即处理,立即处理,立即处理,重要事情说三遍    if err != nil {        // handle        return     }    fmt.println(value);}
  • 使用 errors.New() 可以返回一个 error 类型的对象,注意这是一个接口(指针),这样可以让 == 比较操作正确运行。error 对象可以很好的代替 C++ 的错误码。

C++ 中为了定义错误码和打印错误字符串,往往需要同时维护一个数字宏和字符串宏,需要用某种特殊的宏写法才能实现。go 语言则天然每个错误(码)都自带输出字符串。但是如果随意的 return errors.New("..."),容易造成大量的“没有名字”的错误对象,处理起来并不方便。

  • 预定义全局的 error 对象作为错误码:
代码语言:javascript
复制
var (    // 类似定义错误码,可以全局使用    ZJTenantCodeMissing = errors.New("zhijian: missing tenant code.");    ZJUserIDMissing = errors.New("zhijian: missing user id.");)
type User struct {    UserID string    UserName string}
func getUser(UserID string) (User, error) {    if UserID == '' {        return ZJUserIDMissing // 返回错误(码)    }    // coding...}
func main() {    user, err := getUser('123')    if err == ZJUserIDMissing { // 检查错误(码)        // 处理user id missing    }}
  • 使用 errors.New() 能返回的是对象而不带更多的信息,所以也有定义一个新的错误类型,只要实现了 error 接口就可以了。判断的时候不用 == ,而是用 err.type 的类型进行判断。这个做法比纯“错误码”的用法能带来更多的错误处理细节。
  • 如果希望使用 try...catch 的方式处理错误,可以:
    • throw:panic() 函数
    • catch:被 defer 的函数,实际上是被 finally 时机运行的
    • try:就是从 defer 异常处理函数开始,到本函数结束
    • 在此函数内部,使用 recover() 获得异常对象,然后进行处理
    • 在可能抛出异常的代码前,defer 一个异常处理函数
    • 使用 panic() 函数抛出 error,调用后 panic() 后面的代码将不再执行
    代码语言:javascript
    复制
    func error_process() {    err := recover()    if err != nil { // catch(err) {...}        fmt.Println("catch:", err)    }
        //finally {...}    fmt.Println("finally...")}
    func main() {    defer error_process()        // try{...}    panic("My panic error")      // throw    fmt.Println("After panic")   // it would not be run}

并发支持

  • 关键字 go 启动一个 goroutine,概念上类似线程
  • Channel:一个能阻塞 goroutine 的队列,用 chan 作为符合类型关键字,用 <- 作为读写操作符
  • 对 Channel 读写时,如果没有空间继续操作,会阻塞
代码语言:javascript
复制
func fibonacci(n int, c chan int) {    x, y := 0, 1    for i := 0; i < n; i++ {        fmt.Println("start ch <-", x)        c <- x // 如果管道没空间了,会阻塞在这里        fmt.Println("end ch <-", x)        x, y = y, x+y    }    close(c)}
func main() {    var c chan int    c = make(chan int, 1) // 管道中 2 个空间    go fibonacci(10, c)    for i := range c {  // 管道为空,就会阻塞在此        fmt.Println(i, "<- ch")    }}
/* 输出结果start ch <- 0                                                                                                                       end ch <- 0                                                                                                                         start ch <- 1                                                                                                                       end ch <- 1                                                                                                                         start ch <- 1                                                                                                                       0 <- ch                                                                                                                             1 <- ch                                                                                                                             1 <- ch                                                                                                                             end ch <- 1                                                                                                                         start ch <- 2                                                                                                                       end ch <- 2                                                                                                                         start ch <- 3                                                                                                                       end ch <- 3                                                                                                                         start ch <- 5                                                                                                                       2 <- ch                                                                                                                             3 <- ch                                                                                                                             5 <- ch                                                                                                                             end ch <- 5                                                                                                                         start ch <- 8                                                                                                                       end ch <- 8                                                                                                                         start ch <- 13                                                                                                                      end ch <- 13                                                                                                                        start ch <- 21                                                                                                                      8 <- ch                                                                                                                             13 <- ch                                                                                                                            21 <- ch                                                                                                                            end ch <- 21                                                                                                                        start ch <- 34                                                                                                                      end ch <- 34                                                                                                                        34 <- ch  */

反射与 Tag 注解

  • 需要 import "reflect"
  • reflect.TypeOf()/reflect.ValueOf() 可以返回一个对象的类型和值
  • reflect.methodByName()/reflect.FieldByName() 用一个字符串返回方法,成员
  • 反射字段 Field.Tag 可以获得注解数值字符串
    • 如果 Tag 是类似 `key1:"value1", key2:"value2"` ,还可以通过 Get("key1") 的方法获得 value1 的内容
  • interface{} 是一种特殊的类型,任何的对象都可以转化这个类型的变量,类似 C++ 中的 void*,在反射代码中非常常见,用于存放未知类型的变量
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 韩大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 模块管理
  • 语法差异
  • 内存管理
  • 面向对象
  • 错误处理
  • 并发支持
  • 反射与 Tag 注解
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档