摘要

在上一篇文章中,我们聊了聊Golang中的一些基础的语法,如变量的定义、条件语句、循环语句等等。他们和其他语言很相似,我们只需要看一看它们之间的区别,就差不多可以掌握了,所以作者称它们为“基础语法”。在这篇文章中,我们将聊一聊Golang的一些语言特性,这也是Golang和其他语言差别比较大的地方。除此之外,还有一部分内容是关于Golang的并发,这一部分将在下一篇文章中介绍。

1 结构体

在Java中,我们已经体会过了面向对象的方便之处。我们只需要将现实中的模型抽象出来,就成为了一个类,类里面定义了描述这个类的一些属性。

而在Golang中,则没有对象这一说法,因为Golang是一个面向过程的语言。但是,我们又知道面向对象在开发中的便捷性,所以我们在Golang中有了结构体这一类型。

结构体是复合类型,当需要定义类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。

组成结构体类型的那些数据成为字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。

我们可以近似的认为,一个结构体就是一个类,结构体内部的字段,就是类的属性。

注意,在结构体中也遵循用大小写来设置公有或私有的规则。如果这个结构体名字的第一个字母是大写,则可以被其他包访问,否则,只能在包内进行访问。而结构体内的字段也一样,也是遵循一样的大小写确定可用性的规则。

1.1 定义

对于结构体,他的定义方式如下:

type 结构体名 struct {
字段1 类型
字段2 类型
}

1.2 声明

对于结构体的声明和初始化,有以下几种形式:

使用var关键字

var s T
s.a = 1
s.b = 2

注意,在使用了var关键字之后不需要初始化,这和其他的语言有些不同。Golang会自动分配内存空间,并将该内存空间设置为默认的值,我们只需要按需进行赋值即可。

使用new函数

type people struct {
name string
age int
} func main() {
ming := new(people)
ming.name = "xiao ming"
ming.age = 18
}

使用字面量

type people struct {
name string
age int
} func main() {
ming := &people{"xiao ming", 18}
}

1.3 区别

上面我们提到了几种结构体的声明的方法,但其实这几种是有些区别的。

先说结论,第一种使用var声明的方式,返回的是该实例的结构类型,而第二第三种,返回的是一个指向这个结构类型的一个指针,是地址。

注意,这一部分作者可以保证是观点是正确的。但是作者的解释其实有些问题。这是因为作者能力有限,还没开始研究Golang的源码,所以不能很好的解释“返回的是实例的结构类型”这一句话。在作者的理解中,返回类型有两种,一种是具体的数值,一种是指向这个数值的指针。

所以,对于第二第三种返回指针的声明形式,在我们需要修改他的值的时候,其实应该使用的方式是:

(*ming).name = "xiao wang"

也就是说,对于指针类型的数值,应该要先用*取值,然后再修改。

但是,在Golang中,可以省略这一步骤,直接使用ming.name = "xiao wang"。尽管如此,我们应该知道这一行为的原因,分清楚自己所操作的对象究竟是什么类型,掌握这点对下面方法这一章节至关重要。

2 方法

在上一节的内容中,我们也提到了面向对象的优势,而Golang又是一种面向过程的语言。在上一章节中,提到了用结构体实现了对象这一概念。在这一章中,提到的是对象对应的方法

在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go的 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。

说白了,方法就是函数,只不过是一种比较特殊的函数。

我们都知道,在Golang中,定义一个函数是这样的:

func 函数名(args) 返回类型

而在此基础上,在func函数名之间,加上接受者的类型,就可以定义一个方法。

type Vertex struct {
X, Y float64
} func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
} func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

可以看到,我们定义了一个Vertex为接收者的方法。也就是说,这个方法,仅仅可以被Vertex的结构体数值调用。

注意,接受者有两种类型,即指针接收者和非指针接受者。

我们来看下面的代码:

type Vertex struct {
X, Y float64
} func (v Vertex) test1(){
v.X++;
v.Y++;
} func (v *Vertex) test2(){
v.X++;
v.Y++;
}

在这里我们定义了两个方法,test1test2,他们唯一的区别就是方法名前面的接收者不同,一个是指针类型的,一个是值类型的。

并且,执行这两个方法,也需要定义不同的结构体类型。

v1 := Vertex{1, 1}
v2 := &Vertex{1, 1} v1.test1()
v2.test2() fmt.Println(v1)
fmt.Println(v2)

执行之后我们可以查看结果:

{1 1}
&{2 2}

也就是说,只有指针接收者类型的方法,才能修改这个接收器的成员值,非指针接收者,方法修改的只是这个传入的指针接收者的一个拷贝

那么为什么会这样,我们同样拿代码说话:

type Vertex struct {
X, Y float64
} func (v Vertex) test1(){
fmt.Printf("在方法中的v的地址为:%p\n", &v)
v.X++;
v.Y++;
} func main() {
v1 := Vertex{1, 1}
fmt.Printf("自己定义的v1内存地址为:%p\n", &v1)
v1.test1()
}

在上述的代码中,我定义了一个非指针类型接收者的方法,然后打印方法外的v1和方法内的v的内存地址,结果如下:

自己定义的v1内存地址为:0xc00000a0e0
在方法中的v的地址为:0xc00000a100

我们可以看出,这两个结构体数值的内存地址是不一样的。也就是说,就算我们修改了方法内的数值,对方法外的原变量也不能起到任何的作用,因为我们修改的只是一个结构体数值的拷贝,没有真正的修改的他本来的值。

但是,如果使用的是指针接收者,他们的内存地址就是一样的了,下面看代码:

type Vertex struct {
X, Y float64
} func (v *Vertex) test2(){
fmt.Printf("在方法中的v的地址为:%p\n", v)
v.X++;
v.Y++;
} func main() {
v1 := &Vertex{1, 1}
fmt.Printf("自己定义的v1内存地址为:%p\n", v1)
v1.test2()
}

执行之后的结果为:

自己定义的v1内存地址为:0xc00000a0e0
在方法中的v的地址为:0xc00000a0e0

所以我们可以知道,使用指针接收器是可以直接拿到原数据所在的内存地址,也就是说可以直接修改原来的数值。这也和Java中的对象调用方法更加相似;而对于非指针,它是拷贝原来的数据。至于使用哪一种,需要按照实际的业务来处理。

但是,如果是一个大对象,如果也采用拷贝的方式,将会耗费大量的内存,降低效率。

还有一点需要补充说明:不管是指针接收者还是非指针接收者,他在接受一个对象的时候,会自动将这个对象转换为这个方法所需要的类型。也就是说,如果我现在有一个非指针类型的对象,去调用一个指针接收者的方法,那么这个对象将会自动被取地址然后再被调用。

换句话说,方法的调用类型不重要,重要的是方法是怎么定义的。

3 接口

在聊接口怎么用之前,我们先来聊聊接口的作用。

在作者看来,接口是一种规范,一种约定。举个例子:一个商品只要是符合某种种类的约定,遵循某种种类的规范,那么就可以认为这个商品是属于这个种类的,他会具有这个种类应有的一切功能。这样做的目的是为了把生产这个商品的生产者和使用这个商品的消费者分开。用编程里面的术语来讲,我们可以把实现和调用解耦。

下面举个鸭子模型的例子,来自于知乎,可以说特别的形象生动了。注意,在这里先不研究语法,语法的问题我们后面会提到,你只需要跟随作者的思路去思考:

首先我们定义一个规范,也就是说定义一个接口:

type Duck interface {
Quack() // 鸭子叫
DuckGo() // 鸭子走
}

这个接口是鸭子的行为,我们认为,作为一只鸭子,它需要会叫,会走。然后我们再定义一只鸡:

type Chicken struct {
}

假设这只鸡特别厉害,它也会像鸭子那样叫,也会像鸭子那样走路,那么我们定义一下这只鸡的行为:

func (c Chicken) Quack() {
fmt.Println("嘎嘎")
} func (c Chicken) DuckGo() {
fmt.Println("大摇大摆的走")
}
注意,这里只是实现了 Duck 接口方法,并没有将鸡类型和鸭子接口显式绑定。这是一种非侵入式的设计。

然后我们让这只鸡,去叫,去像鸭子那样走路:

func main() {
c := Chicken{}
var d Duck
d = c
d.Quack()
d.DuckGo()
}

执行之后我们可以得到结果:

嘎嘎
大摇大摆的走

也就是说,这只鸡,他能做到鸭子能做的所有事情,那么我们可以认为,这只鸡,他就是一个鸭子

这里牵涉到了一个概念,任何类型的数据,它只要实现了一个接口中方法集,那么他就属于这个接口类型。所以,当我们在实现一个接口的时候,需要实现这个接口下的所有方法,否则编译将不能通过。

理解了接口是什么之后,我们再来聊聊语法,首先是如何定义一个接口:

type 接口名 interface {
方法1(参数) 返回类型
方法2(参数) 返回类型
...
}

这一部分和结构体的定义很相似,但是里面的元素换成了函数,但是这个函数不需要func关键字。这里和Java中的接口相似,不需要访问修饰符,只需要函数名参数返回类型。

定义完接口之后,我们需要定义方法去实现这些接口。注意,这里新定义方法的方法名参数返回类型,必须和接口中所定义的完全一致

其次,这里的方法中的接收者,就是调用这个方法的对象类型。换句话说,这个方法想要被哪类对象执行,接收者就是谁。

还有最重要的一点,如果要实现某个接口,必须要实现这个接口的全部方法。

在调用接口的时候,我们需要先声明这个接口类型的变量,如我们上面定义了一个Duck接口,就应该声明一个Duck类型的变量。

var d Duck

然后我们把实现了这个方法的接收器对象赋值给这个变量d

d := Chicken{}

随后,我们就可以用这个变量d,是调用那些方法了。

写在最后

首先,还是很感谢你能看到这里,谢谢你!(鞠躬

这是《Golang入门》系列的第三篇了,也差不多要讲完Golang的基本语法了。在这篇文章中是介绍了一些Golang的比较特殊的用法,希望能够对你有所帮助。

当然了,作者也只是个初学者,也才刚刚开始学习Golang。在学习的过程中,难免会有错误的理解,或者会有遗漏的地方。如果你发现了,请你留言指正我,谢谢!除此之外,如果有我讲的不清楚不明白的地方,也欢迎留言,我们一起交流学习。

在下一篇中,作者将介绍一下Golang的并发。我们下篇文章见。

再次感谢~

PS:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~

Golang入门(3):一天学完GO的进阶语法的更多相关文章

  1. Golang入门(2):一天学完GO的基本语法

    摘要 在配置好环境之后,要研究的就是这个语言的语法了.在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆.因为这篇文章只是入门Golang的第二 ...

  2. 0基础如何更快速入门Linux系统?学完Linux有哪些就业方向?

    Linux系统是使用Linux内核及开源自由软件组成的一套操作系统,是一种类UNIX系统,其内核在1991年10月5日由林纳斯·托瓦兹首次发布. 它的主要特性:Linux文件一切皆文件.完全开源免费. ...

  3. Golang入门(4):并发

    摘要 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要.Web服务器会一次处理成千上万的请求,这也是并发的必要性之一.Golang的并发控制比起Java来说,简单了不少.在Go ...

  4. Golang入门(1):安装与配置环境变量的意义

    摘要 在几年前学习Java的时候,环境的配置就会劝退一部分的初学者.而对于Golang来说,也需要从环境的配置开始学起.这一篇文章将从如何安装Golang开始讲起,随后将会提到Golang中的环境变量 ...

  5. 3分钟学完Python,直接从入门到精通

    作为帅气小编,我已经把python一些模块的甩在这儿了qwq,只要你拿到这些干货,包你玩转python,直接冲向"大佬"的段位,如果已经学了C或者C++或者说如果你需要你的一段关键 ...

  6. Java程序员的Golang入门指南(下)

    Java程序员的Golang入门指南(下) 4.高级特性 上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号.函数多返回值.switch-case默认break.函数闭包.集合 ...

  7. 在w3cschool学完html,css,javascript,jquery以后,还是不会做前端怎么办?

    w3cschool是一个非盈利性的在线技术学习网站,提供按W3C标准编写的基础教程.完整的看完w3cschool上面的手册,可以基本掌握编程语法.基础性的东西通常都会比较零散,因此,在学习一段时间后, ...

  8. Golang 入门 : channel(通道)

    笔者在<Golang 入门 : 竞争条件>一文中介绍了 Golang 并发编程中需要面对的竞争条件.本文我们就介绍如何使用 Golang 提供的 channel(通道) 消除竞争条件. C ...

  9. 为什么学完C语言觉得好像没学一般?

    不少同学从Hello world学到文件操作之后,回顾感觉会又不会? 学会了又感觉没学会?这种不踏实.模糊虚无的感觉?   原因在于编程不同于理论学科,你听懂和理解了理论就可以运用. 比如历史地理,看 ...

随机推荐

  1. django学习笔记 多文件上传

    习惯了flask 再用django 还是不太习惯  好麻烦 配置文件也忒多了 不过还是要学的 之前只能一个一个文件长传,这次试试多个文件 不适用django的forms创建表单 直接在html中使用 ...

  2. 如何使用Kibana

    目录 前言 一.安装 二.加载自定义索引 三.如何搜索数据 四.如何切换中文 五.如何使用控制台 六.可视化图表 七.使用仪表盘 前言 Kibana 是为 Elasticsearch设计的开源分析和可 ...

  3. MySQL记录操作(单表查询)

    单表查询的语法及关键字执行的优先级 单表查询语法 SELECT DISTINCT 字段1,字段2... FROM 表名 WHERE 条件 GROUP BY field HAVING 筛选 ORDER ...

  4. 从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用

    环境 OS Win10 CPU 4核8线程 IDE IntelliJ IDEA 2019.3 JDK 1.8 -server模式 场景 最初的代码 一个线程A根据flag的值执行死循环,另一个线程B只 ...

  5. Scapy编写ICMP扫描脚本

    使用Scapy模块编写ICMP扫描脚本: from scapy.all import * import optparse import threading import os def scan(ipt ...

  6. Django之CBV装饰器,跨站请求伪造,auth认证

    CBV加装饰器 基于session实现登录 def login(request): if request.method == 'POST': username = request.POST.get(' ...

  7. http server部署discuz

    httpd服务器搭建discuz 第一步.数据库的配置 create database discuz; grant all privileges on discuz.* to 'discuz'@'lo ...

  8. [二分,multiset] 2019 Multi-University Training Contest 10 Welcome Party

    Welcome Party Time Limit: 4000/4000 MS (Java/Others)    Memory Limit: 524288/524288 K (Java/Others)T ...

  9. stm32CubeMx+TrueSTUDIO+uc/os-III移植开发(一)

    自从接触到stm32cubemx后,发现使用起来很方便,用来做项目开发的话,由于是图形化操作界面,工作效率比较快.如果要学习操作系统,以前的IDE如keil,IAR等IDE对操作系统的代码调试起来不够 ...

  10. VBScript - 动态 Array 实现方法大全!

    记录一些方法,关于 VBScript 中,动态 Array 的实现 ,也适用于 VBA, 很久以前,写 VBA 的时候,就觉得使用 Array 很不方便,因为大小固定, 当时想的是,要是 Array ...