Go语言学习笔记、Go和C++区别

Go语言学习笔记、Go和C++区别

1.前言
本文基于C++98标准,结合个人一点C++编程的经验,讨论C++和Go语言的差异。

2.技术选型

技术选型 优点 缺点
PHP+Redis LAMP架构,方便开发 无法进行内存级别的控制、很多复杂的事情不方便、无法构造复杂的数据结构,并且常驻内存程序有很多问题、内存泄露、并发性能有限
Python+Redis 脚本语言开发迅速、数据结构丰富、也有多线程等模式 无法处理复杂数据结构、需要做C扩展或者使用很多复杂的实现方式,常驻内存后端程序还是不够稳定,无法处理非常高性能的场合,也不方便优化
C++ +Redis 高性能、极强的定制性、可以实现任何内存和数据结构、可以达到任何功能和性能的要求和强大的底层操控能力 开发维护成本高、出现问题几率较大、没有专职C++开发人员,需要构建很多基础库、包括异步网络框架,采用多线程还是异步IO、配置文件解析、内存管理、日志等基础库的开发
Java +Redis 可以高性能、有GC、比较成熟、有很多文档和资源、语言普及 NIO等工作方式性能不是很高 代码太庞大

Node.js+Redis 高性能高并发 开发简单方便,会写JS就能开发模块 对内存操作支持较弱,有时候无法满足一些要求,编程方式是异步时间方式,开发起来不是太方便

Erlang、Haskell、Scala+Redis 高性能、定制性强、高并发、Erlang特别适合分布式的大型业务 函数式编程语言学习维护成本较高、后续面临没有人维护的情况,有部分复杂数据结构不确定是否能够实现

Nginx_lua+Redis 方案成熟,利用Nginx的高性能使用lua控制策略 架构线有点长,我们后续服务需要的某些场景无法满足,特别是内存操作比较薄弱

Go+Redis 学习曲线短、高性能、高并发,支持强大的的类C的内存和各种数据结构操作可以满足一般场景需求 目前golang的基础库不全,语言出现时间比较短,资料不是很多

3.= 和 :=
在C++中没有:=这个符号
在go语言中:=用来声明一个变量的同时给这个变量赋值 并且只能在函数体内使用
主要是为了省略类型声明,系统会自己选择相应的类型识别定义的变量

这个就不对了
例如定义
i := 3
fmt.Println(i)
是可以的

但是
var i int := 3
//i := 3
fmt.Println(i)
是错误的

注意赋值的一个问题例如:
i :=1
s := []sring{“A”,”B”,”C”}
i,s[i-1] = 2,”Z”
这里i-1先执行 所以 等于以下语句
s[0] = “Z”
i=2
4.New和make操作符
在C++中new操作符用于给一个对象 分配内存但是没有清零
例如 char *sta = new charf[100];
然后要 memset(sta , 0, 100);避免乱码问题

在go语言中
他并不会初始化分配到的内存,只会清零
调用new(T)被求值时 所做的是为T类型的新值分配并且清零一块内存空间,然后将内存空间的地址作为结果返回 所以不用担心乱码问题 可以直接拿来使用

New和make的差别
1、make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
2、new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:new返回指针。
3、make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型(引用),而不是*T。
4、本质来讲,导致这三个内建类型有所不同的原因是:引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。make返回初始化后的(非零)值。
5、故make 是内建类型初始化的方法,例如:s :=make([]int,len,cap) //这个切片在元素在超过10个时,底层将会发生至少一次的内存移动动作

5.指针不能作为数组的index

这里只能定义指向数组的指针 不能用指向int的指针

但是这样是可以的

也就是 要传递的必须是原来这个类型的指针
这里要讲一个经典的C的面试题 即下列表达式的含义是什么
int ((*funcs[4])(int ()(int, int), int, int))(int, int);
分析:这是一行定义语句,C语言最难理解的便是其花样繁多的声明/定义。理解声明/ 代表一段“逻辑”内存)出发,看变量经过怎样的操作(注意操作顺序),最终得到什么 回答:
1. 变量名funcs,可以进行[4] 下标操作,funcs是一个数组;
2. funcs数组的元素可以被*解析,说明数组元素是指针,funcs是指针的构成的数组;
3. 指针指向的对象,可以进行()调用操作,说明指向的对象是函数,funcs是函数指针的构成的数组;
4. funcs数组的元素指向的函数,有三个参数:int(*)(int,int)、int、int,第一个参数是一个函数指针,它的参数是两个int变量,返回值是int型
5. funcs数组的元素指向的函数的返回值可被*解析,说明返回值是个指针,这个指针解析后又可以进行()函数调用,说明仍然是个函数指针
6. 返回的函数指针指向的函数,参数是2个int变量,返回结果是int
定义了一个有4个元素的数组,数组元素是函数指针,指向的函数以int(*)(int,int)、int、int为参数,返回是一个函数指针。
返回的函数指针以int int为参数,返回值是int
结论: C语言就是在折腾“逻辑”内存(线性空间),我们声明一段内存可以做怎样的解释,代表什么样的东西,然后对这个东西进行操作。

在go语言中 不管是什么指针类型都是在 表达式的左边加一个*就表示该类型的指针,这样避免了语义上面的理解障碍。要说来上面这个表达式我也看不懂,建议不要这样编程,也没有这样的需求,分开写,用几个typedef。
6.数组之间可以用== 和!=进行比较
在C++中
If(a=b)是合法的 是判断这个赋值是否成功(这个有不成功的时候,例如拷贝构造函数出错了) 而且这个会执行 将b的值赋给a
但是go语言里面是编译不过的

另一个是Go中低层实现了两个数组的比较数组比较是合法的表达式。但是在C和C++语言中数组名称表示该数组的首地址,比较是地址之间比较,如果是两次声明的不同变量不可能相等,所以这样的表达式没有意义。

7.Go的面向对象编程
C++面向对象太复杂就不写了(可以写N本书),go语言和c++在面向对象方面完全没有可比性,尤其是没有继承、泛型、虚函数、函数重载、构造函数、析构函数等。

结构struct
1、go中的struct与C中的struct非常类似,go没有class
2、使用typestruct{}定义结构,名称遵循可见性规则
3、支持指向自身指针类型成员
4、支持匿名结构,可用作成员或定义成员变量
5、匿名结构也可以用于map的值
6、可以使用字面值对结构进行初始化
7、允许直接通过直阵来读写结构成员
8、相同类型的成员可进行直接拷贝赋值
9、支持==与!=比较运算符,但不支持><
10、支持匿名字段看起来像继承,但是不是继承
11、可以使用匿名字段指针
结构体赋值的方法代码如下:
package main

import (
“fmt”
)
type human struct{
Sex int
}

type teacher struct{
human
Name string
Age int
}

type student struct{
human
Name string
Age int
}

package func main() {
a :=teacher{Name: “joe”,Age: 19,human: human{Sex: 0}}
a :=student{Name: “joe”,Age: 19,human: human{Sex: 0}
fmt.Println(a,b)
}

方法method
1、go中的方法是通过receiver来实现与某个类型的组合
2、只能为同一个包中的类型定义方法
3、Receiver可以是类型的值或者指针
4、没有方法重载
5、可以使用指针或者值来调用方法,编译器自动完成转换
6、如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
方法可以调用结构中的非公开字段
1. 值与引用
varName2 = varName1
varName2.funcName()
如果 varName2 有变化,varName1 无变化,则为值类型(传递)
如果 varName2 有变化,varName1 有变化,则为引用类型(传递)

  1. Method
    格式:
    func (r [*]ReceiverType) funcName(param) (result) {…}
    注:
    1、 r 为struct对象的接收者,接收者不同,方法也不同
    2、r 可以为值传递也可以为引用传递
    3、param 和 result 可有可无,与普通函数一样
    4、调用方法,用 “.” 连接,即 r.funcName()
    5、func (r *ReceiverType) funcName()和 func (r ReceiverType) funcName 不能同时存在(此处funcName相同)
    6、method 可以用于所有的类型
    例:
    type Circle struct {
    radius float64
    }

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println(“Area of Circle(c1) = “, c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

用面向对象的方式表示,struct == class,struct 中声明的元素为成员属性,Circle 作为接收者的函数为成员方法
即 Circle 类名,radius 与 getArea() 分别是 Circle 类对象 c1 的成员属性和成员方法
例1 (值传递):
type Circle struct {
radius float64
}

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println(“main(): radius = “, c1.radius)
fmt.Println(“Area of Circle(c1) = “, c1.getArea())
}

func (c Circle) getArea() float64 {
fmt.Println(“getArea(): radius = “, c.radius)
return 3.14 * c.radius * c.radius
}

func (c Circle) setRadius(r float64) {
c.radius = r
fmt.Println(“setRadius(): radius = “, c.radius)
}
结果:
setRadius(): radius = 10
main(): radius = 0
getArea(): radius = 0
Area of Circle(c1) = 0
例2 (引用传递)
type Circle struct {
radius float64
}

func main() {
var c2 Circle
c2.setRadius(10.00)
fmt.Println(“main(): radius = “, c2.radius)
fmt.Printf(“Radius of Circle(c2) = %.2f, area = %.2f”, c2.radius, c2.getArea())
}

func (c *Circle) setRadius(r float64) {
c.radius = r
fmt.Println(“setRadius(): radius = “, c.radius)
}

func (c *Circle) getArea() float64 {
fmt.Println(“getArea(): radius = “, c.radius)
return 3.14 * c.radius * c.radius
}
结果:
setRadius(): radius = 10
main(): radius = 10
getArea(): radius = 10
Radius of Circle(c2) = 10.00, area = 314.00
补:
method可用于所有类型
定义格式:
type 自定义类型 类型
如: type Integer int,类似于给 int 定义一个别名,但是此时的 Integer 与 struct 定义的类型使用方法一样,把它当作一个类,可以为其添加方法
例:
type Integer int
func main() {
var i Integer
fmt.Println(i.getType())
fmt.Println(Integer.getType(i))
Integer.Print(i, “I am Int”)
}

func (i Integer) getType() string {
return “Integer == int”
}

func (i Integer) Print(s string) {
fmt.Println(s)
}
结果:
Integer == int
Integer == int
I am Int
注:
1、接收者为 typeName,而不是 *typeName 时,调用方法 varName.funcName(param) 与 typeName.funcName(varName, param) 一样,如果是 *typeName 则不能后者访问
2、如果者为 *typeName 时,通过 varName.funcName() 访问时,等于 (&varName).funcName(),即可以不用在 varName 前面加上 “&” 取地址

  1. Method继承与重载
    匿名字段就类似于面向对象编程中的继承成员属性,也可以重载成员属性,method 同样可以继承和重载
    3.1 继承示例:
    类似于调用父类中有而子类中没有的方法
    没有面向对象中的private protected public关键字(通过方法名首字母大小写来判断)
    type Person struct {
    name string
    age int
    }

type Employee struct {
Person
salary int
}

func main() {
// var em1 Employee = Employee{Person{“rain”, 23}, 5000}
em1 := Employee{Person{“Rain”, 23}, 5000}
em1.printMsg()
}

func (p Person) printMsg() {
fmt.Println(“I am “, p.name, ” , and my age is “, p.age)
}
结果:
I am Rain , and my age is 23
3.2 重载示例
重载方法,通过 varName.funcName() 访问时,也采用最外层优先访问的原则,也类似于面向对象中调用本类中的方法
通过 varName.匿名字段.funcName() 访问指定匿名字段中的方法,类似于面向对象中调用父类中的方法
type Person struct {
name string
age int
}

type Employee struct {
Person
salary int
}

func main() {
// var em1 Employee = Employee{Person{“rain”, 23}, 5000}
em1 := Employee{Person{“Rain”, 23}, 5000}
em1.printMsg() //调用最外层(本类)的方法
em1.Person.printMsg()//调用指定匿名字段(父类)的方法
}

func (p Person) printMsg() {
fmt.Println(“I am “, p.name, ” , and my age is “, p.age)
}

func (e Employee) printMsg() {
fmt.Printf(“I am %s, my age is %d, and my salary is %d. \n”, e.name, e.age, e.salary)
}
结果:
{{rain 23} 5000}
I am rain, my age is 23, and my salary is 5000.
I am rain , and my age is 23

函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

8.Vector和slice
切片Slice
1、其本身并不是数组,它指向底层的数组
2、作为变成数组的替代方案,可以关联底层数组的局部或全部为引用类型
3、可以直接创建或从底层数组获取生成
4、使用len获取元素个数,cap获取容量
5、一般使用make创建
6、使用多个slice指向相同的底层数组,其中一个值改变会影响全部,但是有一个例外就是该slice突破容量限制时,会重新分配内存然后 就不会引起这个改变了
7、在make([]T,len,cap)中 cap可以省略 则和len的值相同

Go语言在语言层面对数组切片的支持,相比C++开发者有效地消除了反复
写以下几行代码的工作量:

include

include

include

using namespace std;

切片主要有一个好处不用for循环来赋值
vector::iterator it;
Vector myvector;
for(it=vec.begin();it!=vec.end();it++)
{
Myvector.push_back(*it);
}

values := [] int{1,2,3,4,5,6,7,8,9,10}
valuesnew = values[:len(values)]
go语言数据内存结构如下
基础类型
让我们从一些简单的例子开始:

变量i是int类型,在内存中占用一个32位的存储单位。(上图拿32位系统来举例;对以上的例子,只有指针才会在64位的机器上占用更多的空间——int始终是32位——然而我们仍然可以选择64位的系统。)
变量j是int32类型,因为它经过了显式的类型转化。尽管i和j有着同样的内存布局,但它们的类型是不一样的:像这样的赋值i = j会产生类型异常,必须通过显式的类型转换:i = int(j) 。
变量f是个浮点类型,上例中它代表着占用32位的浮点值。它的内存占用跟int32一样,但内部布局不同。
结构与指针
变量bytes是[5]byte类型,一个具有5个字节的数组。它的内存表示就只有5个紧挨着的字节,就像C里的数组一样。相似地,primes变量是一个拥有4个int数值的数组。
Go就像C而不像Java,它让程序员决定什么是或者不是一个指针。拿这个类型定义来举例:
type Point struct{X,Y int}
定义一个叫Point的简单的结构类型,意味着内存里是两个相邻的int。

Point{ 10, 20 }这句复合语法表示一个被初始化的Point对象。而&Point{ 10, 20 }这句则表示一个指向被初始化的Point对象的指针。前者在内存中有两个数据块,而后者则存放着一个指向两个数据块的指针。
结构中的字段被依次地排列在内存里面。
type Rect1 struct {Min,Max Point}

type Rect2 struct {Min,Max *Point}

Rect1是一个拥有两个Point类型字段的结构,它的一条记录包含了两条Point记录——共4个int。Rect2是一个拥有两个Point类型指针的结构,在内存里它占两个Point指针的空间。
用过C的程序员也许对Point字段和*Point字段的区别并不陌生,而只用过Java或者Python(或者其他)则可能为需要做出选择而惊讶。通过 为程序员提供控制基础内存布局的可能,Go语言让程序员可以操控所有数据结构总尺寸、所分配变量的总数和内存访问的模式,这些对于建造高性能系统都至关重 要。

字符串

(灰色箭头意味着实现上的真实表示方式,但这在编程过程中是不可见的)

一个字符串在内存中的表示被分成两段,一个指向字符串数据的指针和一个长度值。因为字符串是可枚举的,所以多个字符串共享同一段存储空间也是安全的,因此 如果对s字符串进行一个切片选择,将得到一个可能不一样的指针和长度,但它们也指向同一段字节序列。这意味着,切片并不需要分配空间或者是复制数据,创建 切片很容易,只需要传递明确的下标值就行了。
(顺带一句,在Java和某些与严重有一个著名的缺陷,当你对一个字符串进行切片并保存其中的一小部分,引用将在内存中保存原字符串的完整内容,即使只有 很小的一部分是被用到的。Go也有这个缺陷。要不然(我们尝试但最终舍弃了),我们将对切片采取昂贵的做法——分配内存并拷贝数据——大部分语言都避免这 种做法。

切片

切片是对数组中一段数据的引用。在内存中它有三段数据组成:一个指向数据头的指针、切片的长度、切片的容量。长度是索引操作的上界,如:x[i] 。容量是切片操作的上界,如:x[i:j] 。
跟对字符串做切片一样,对数组进行切片也不会导致复制:它只创建一个存放指针、长度和容量的结构体。在这个例子中,语句[ ] int { 2, 3, 5, 7, 11 } 创建了一个包含5个值的新数组,并为x切片设置了对应的值来描述那个数组。切片表达式x[1:3]并不为数据分配内存:它只填充切片结构的字段,用以复用 数组的存储空间。在这个例子中,长度是2,y[0]和y[1]是仅有的合法数据;但容量是4,y[0:4]是个合法的切片表达式。(查看高效GO获取更多关于长度、容量,以及如何使用切片的信息。)
由于切片不是指针而是多字段的结构,切片操作并不需要分配内存,即使对于切片头也是这样,它可以常驻在栈中。这种表示法让切片的使用的代价很低,就像C中 传递精确的指针和长度一样。Go原生地在切片中使用了指针,这也意味着每个切片操作都分配一个内存对象。即使有了一个更快的内存分配器,这为垃圾回收带来 了不必要的工作,并且我们发现,就像字符串那个例子一样,给于精确的下标,比进行切片操作好。大多数情况下,避免不必要的间接引用和内存分配可以让切片足 够高效了。

new和make
Go有两种创建数据结构的方法:new和make 。它们的区别是常见的早期困惑,但很快就会变得自然。基础的区别在于,new(T)返回一个*T类型,一个可以被隐性反向引用的指针(如图中的黑色指 针),而make(T,args)返回一个原始的T,它并不是一个指针。T中常有写隐性的指针(如图中的灰色指针)。new返回一个指向初始化为全0值的 指针,而make返回一个复杂的结构。

Slice的长度和容量是不同的,但是在C++中这个容量是系统自己调整的,不可以显示指定。
vector其中一个特点:内存空间只会增长,不会减小,援引C++ Primer:为了支持快速的随机访问,vector容器的元素以连续方式存放,每一个元素都紧挨着前一个元素存储。设想一下,当vector添加一个元素时,为了满足连续存放这个特性,都需要重新分配空间、拷贝元素、撤销旧空间,这样性能难以接受。因此STL实现者在对vector进行内存分配时,其实际分配的容量要比当前所需的空间多一些。就是说,vector容器预留了一些额外的存储区,用于存放新添加的元素,这样就不必为每个新元素重新分配整个容器的内存空间。
在调用push_back时,每次执行push_back操作,相当于底层的数组实现要重新分配大小;这种实现体现到vector实现就是每当push_back一个元素,都要重新分配一个大一个元素的存储,然后将原来的元素拷贝到新的存储,之后在拷贝push_back的元素,最后要析构原有的vector并释放原有的内存。例如下面程序:

include #include #include using namespace std;class Point{

public: Point() { cout << “construction” << endl; } Point(const Point& p) { cout << “copy construction” << endl; } ~Point() { cout << “destruction” << endl; }};int main(){ vector pointVec; Point a; Point b; pointVec.push_back(a); pointVec.push_back(b); cout<

include using namespace std; vector

猜你喜欢

转载自blog.csdn.net/u011983997/article/details/48975381