go学习 6、方法

6、方法

面向对象编程(OOP),封装、组合。

6.1 方法声明

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

package geometryimport "math"type Point struct{ X, Y float64 }// traditional function
func Distance(p, q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
}// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {return math.Hypot(q.X-p.X, q.Y-p.Y)
}

附加的参数p,叫做方法的接收器(receiver)。
在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q))  // "5", method call

第一个Distance的调用 实际上用的是包级别的函数geometry.Distance,而第二个则是使用刚刚声明的Point,调用的 是Point类下声明的Point.Distance方法。
这种p.Distance的表达式叫做选择器,选择合适的对应p这个对象的Distance方法来执行。
选择器也会被用来选择一个struct类型的字段,比如p.X。由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义。

因为每种类型都有其方法的命名空间,我们在用Distance这个名字的时候,不同的Distance调 用指向了不同类型里的Distance方法。

// A Path is a journey connecting the points with straight lines.
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {sum := 0.0for i := range path {if i > 0 {sum += path[i-1].Distance(path[i])} }return sum 
}

我们可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型(这个例子里,底层类型是指[]Point这个slice,Path就是命名类型)不是指针或者interface。

perim := Path{{1, 1},{5, 1},{5, 4},{1, 1},
}
fmt.Println(perim.Distance()) // "12"

对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名。
方法比之函数的一些好处:方法名可以简短。

import "gopl.io/ch6/geometry"
perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}} fmt.Println(geometry.PathDistance(perim)) // "12", standalone function fmt.Println(perim.Distance()) // "12", method of geometry.Path

6.2 基于指针对象的方法

当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法。

func (p *Point) ScaleBy(factor float64) {p.X *= factorp.Y *= factor
}

方法的名字是 (*Point).ScaleBy。括号是必须的
为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许 其出现在接收器中的:

type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type

想要调用指针类型方法 (*Point).ScaleBy ,只要提供一个Point类型的指针即可:

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p) // "{2, 4}"
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Println(p) // "{2, 4}"

go:如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法:

 p.ScaleBy(2)

编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”,我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:

 Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal

我们可以用一个 *Point 这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号来取到该变量即可。

pptr.Distance(q)
(*pptr).Distance(q)

总结

  • 不论是接收器的实际参数和其接收器的形式参数相同,比如两者都是类型T或者都是类 型 *T :
Point{1, 2}.Distance(q) //  Point
pptr.ScaleBy(2)         // *Point
  • 接收器实参是类型T,但接收器形参是类型 *T ,这种情况下编译器会隐式地为我们取变量的地址:
 p.ScaleBy(2) // implicit (&p)
  • 接收器实参是类型 *T ,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量:
 pptr.Distance(q) // implicit (*pptr)

6.2.1 Nil也是一个合法的接收器类型

package urltype Values map[string][]stringfunc (v Values) Get(key string) string{if vs:=v[key];len(vs)>0{return vs[0]}return ""
}func (v Values) Add(key,value string){v[key]=append(v[key],value)
}
m := url.Values{"lang": {"en"}} // direct construction
m.Add("item", "1")
m.Add("item", "2")
fmt.Println(m.Get("lang")) // "en"
fmt.Println(m.Get("q"))    // ""
fmt.Println(m.Get("item")) // "1"      (first value)
fmt.Println(m["item"])     // "[1 2]"  (direct map access)
m = nil
fmt.Println(m.Get("item")) // ""
m.Add("item", "3")         // panic: assignment to entry in nil map

nil的字面量编译器无法判断其准备类型。最后的那行m.Add的调用就会产生一 个panic,因为他尝试更新一个空map。

6.3 通过嵌入结构体来扩展类型

import "image/color"
type Point struct{ X, Y float64 }
type ColoredPoint struct {PointColor color.RGBA
}

我们可以直接认为通过嵌入的字段就是 ColoredPoint自身的字段,而完全不需要在调用时指出Point

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"

可以把ColoredPoint类型当作接收器来调用 Point里的方法,即使ColoredPoint里没有声明这些方法:

red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"

Point类的方法也被引入了ColoredPoint
尽管q有着Point这个内嵌类型,我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误::

 p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point

一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance 和ScaleBy方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:

func (p ColoredPoint) Distance(q Point) float64 {return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {p.Point.ScaleBy(factor)
}

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我 们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一 个*Point的指针。

type ColoredPoint struct {*PointColor color.RGBA
}
p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point                 // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样:

type ColoredPoint struct {Pointcolor.RGBA 
}

这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColoredPoint中的方法。

var (mu sync.Mutex // guards mappingmapping = make(map[string]string)
)
func Lookup(key string) string {mu.Lock()v := mapping[key]mu.Unlock()return v
}
var cache = struct {sync.Mutexmapping map[string]string
}{mapping: make(map[string]string),
}func Lookup(key string) string {cache.Lock()v := cache.mapping[key]cache.Unlock()return v
}

6.4 方法值和方法表达式

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance
fmt.Println(distanceFromP(q))
var origin Point
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)
scaleP := p.ScaleBy // method value
scaleP(2)
scaleP(3)
scaleP(10)
// p becomes (2, 4)
//      then (6, 12)
//      then (60, 120)

time.AfterFunc这个函数的功能是 在指定的延迟时间之后来执行一个(译注:另外的)函数。且这个函数操作的是一个Rocket对象 r

type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })

直接用方法"值"传入AfterFunc的话可以更为简短:

 time.AfterFunc(10 * time.Second, r.Launch)

当调用一个方法时,我们必须要用选择器(p.Distance)语法来指定方法的接收器。
当T是一个类型时,方法表达式可能会写作 T.f 或者 (*T).f ,会返回一个函数"值",这种函 数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:

p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance   // method expression
fmt.Println(distance(p, q))  // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p)            // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(), // 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。
type Point struct{ X, Y float64 }func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }type Path []Pointfunc (path Path) TranslateBy(offset Point, add bool) {var op func(p, q Point) Pointif add {op = Point.Add} else {op = Point.Sub}for i := range path {// Call either path[i].Add(offset) or path[i].Sub(offset).path[i] = op(path[i], offset)} 
}

6.5 封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也
被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
这也就是IntSet被定义为struct类型的原因,尽管它只有一个字段:

type IntSet struct {words []uint64
}

一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
封装的优点:

  • 因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
  • 隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
  • 阻止了外部调用方对对象内部的值任意地进行修改。
    下面的Counter类型允许调用方来增加counter变量的值,并且允许将这个值reset为0,但是不允许随便设置这个值:
type Counter struct { n int }
func (c *Counter) N() int     { return c.n }
func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset()     { c.n = 0 }

只用来访问或修改内部变量的函数被称为setter或者getter,例子如下,比如log包里的Logger 类型对应的一些函数。在命名一个getter方法时,我们通常会省略掉前面的Get前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如Fetch,Find或者Lookup。

package log
type Logger struct {flags  intprefix string// ...
}
func (l *Logger) Flags() int
func (l *Logger) SetFlags(flag int)
func (l *Logger) Prefix() string
func (l *Logger) SetPrefix(prefix string)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/11441.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

gitee中fork了其他仓库,如何在本地进行同步

GitHub 操作:同步 Fork 来的仓库(上游仓库)_sigmarising的博客-CSDN博客 1. 设置upstream 2. git pull --rebase 3. 然后再执行pull、push操作

神经数据库:用于使用 ChatGPT 构建专用 AI 代理的下一代上下文检索系统 — (第 2/3 部分)

书接上回理解构建LLM驱动的聊天机器人时的向量数据库检索的局限性 - (第1/3部分)_阿尔法旺旺的博客-CSDN博客 其中我们强调了(1)嵌入生成,然后(2)使用近似近邻(ANN)搜索…

网络层协议总览

网络层协议总览 IPARP(地址解析协议)ICMP(网际控制报文协议)路由选择协议NAT(网络地址转换协议) 网络层的主要协议包括IP、ARP、RARP、ICMP、IGMP以及各种路由选择协议等。 IP IP协议是TCP/IP协议簇中的核…

Linux解决 Failed to restart NetworkManager.service: Unit not found问题

解决“Failed to restart NetworkManager.service: Unit not found.”问题的步骤 如果你遇到了“Failed to restart NetworkManager.service: Unit not found.”的错误信息,不要担心!这个问题很常见,并且很容易解决。下面是解决这个问题的步…

双指针解决n数之和问题

1. 两数之和 1. 两数之和 将时间复杂度降到O(n)&#xff1b; class Solution {// 双指针public int[] twoSum(int[] nums, int target) {int nnums.length;int l0;while(l<n){int rn-1;// 找到第一个可能nums[l]nums[r]target的位置while(r>l){if(nums[l]nums[r]targe…

flask 实现一个简单的登录

<!--index3.html --> <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"><title>第一个网页标题</title> </head> <body><form action"/userName" METHOD"get&qu…

Intellij IDEA有什么奇技淫巧?

IDEA全称 IntelliJIDEA&#xff0c;是java语言开发的集成环境&#xff0c;IntelliJ在业界被公认为最好的java开发工具之一&#xff0c;尤其在智能代码助手、代码自动提示、重构、J2EE支持、Ant、JUnit、CVS整合、代码审查、创新的GUI设计等方面的功能可以说是超常的。 idea下载…

负载均衡安装配置详解

负载均衡&#xff08;Load Balancing&#xff09;是一种将网络流量分布到多个服务器上的技术&#xff0c;以提高系统的性能、可靠性和可扩展性。 在负载均衡中&#xff0c;有一个负载均衡器&#xff08;Load Balancer&#xff09;&#xff0c;它充当了传入请求的前置接收器。当…

index页面通过<script>引入根目录下的js文件后,vite打包项目后,项目中无js文件解决方法

解决方法&#xff1a; 根据打包报错提示&#xff0c;如图&#xff1a;即在<script>标签中加入&#xff1a;type&#xff0c;如图&#xff1a; 再打包 js文件就会被打包进去&#xff01;

水文监测软件 HYPACK 2023.2 Crack

HYPACK是由美国coastal海洋图像公司出品的一款世界知名的水文综合测量软件。它能够为勘测员提供了设计勘测、收集数据、处理数据、减少数据和生成最终产品所需的所有工具。从大地测量转换、测量设计、数据采集、数据后处理直到最终测量成图都实现了快速可靠&#xff0c;强大的绘…

windows10安装PostgreSQL报错

Failed to load sql modules into the database cluster 原因 在windows10上安装PostgreSQL时报错:Failed to load sql modules into the database cluster 这是因为在安装时,安装包无法自动创建postgres用户 解决方法 将安装失败的软件删除,并将文件夹也一并删除主动创…

Swift 让ScrollView滚动到具体某个位置

1. 使用scrollToItem方法滚动集合视图 DispatchQueue.main.asyncAfter(deadline: .now() 0.1) {let firstIndexPath IndexPath(item: 0, section: 0)let lastIndexPath IndexPath(item: self.recordArray.count - 1, section: 0)// Scroll to first itemself.collectionVie…

【Android】Recyclerview的缓存复用

介绍 RecyclerView是Android开发中常用的一个高度可定制的列表视图组件。它是在ListView和GridView的基础上进行了改进和增强&#xff0c;旨在提供更好的性能和更灵活的布局管理。 RecyclerView的主要特点如下&#xff1a; 灵活的布局管理器&#xff08;LayoutManager&#…

六边形架构和分层架构的区别?

六边形架构和分层架构是什么&#xff1f; 六边形架构&#xff08;Hexagonal Architecture&#xff09;和分层架构&#xff08;Layered Architecture&#xff09;是两种常见的软件架构模式。六边形架构强调将核心业务逻辑与外部依赖解耦&#xff0c;通过接口与外部世界进行通信。…

MySQL DQL语法

MySQL DQL语法 DQL语法简介 DQL&#xff08;Data Query Language&#xff09;语句是一种用于从数据库中检索数据的语言。它主要用于数据查询和数据分析&#xff0c;而不是对数据库中的数据进行更新、插入或删除。DQL语句通常用于获取特定条件下的数据&#xff0c;进行聚合计算…

一文让你彻底搞懂Mybatis之缓存机制

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 Maven版本&#xff1a;apache-maven-3.6.3 Mybatis版本&#xff1a;3.5.6 文章目录 一. 缓存是什么&#xff1f;二. 为什么要使用缓存&#xff1f;三. Mybatis中的缓存分哪几种&#…

pg_13安装

1.安装&#xff08;rpm包安装&#xff09; 1.1 rpm安装 https://www.postgresql.org/download https://yum.postgresql.org/repopackages.php 文档&#xff1a;https://www.postgresql.org/download/linux/redhat/ rpm下载&#xff1a;https://yum.postgresql.org/rpmchart/1…

MySql基本操作

在了解具体的MySql操作之前&#xff0c;我们需要了解一些基本的sql语句注意事项&#xff0c;如下所示&#xff1a; 每一条sql语句都需要以英文 ; 作为结尾&#xff1b;sql语句当中的关键字不区分大小写&#xff0c;不区分双引号和单引号&#xff1b;sql中库名称、表名称和字段…

Springboot+Netty搭建基于UDP协议的客户端(四)

使用NettySpringBoot方式可以快速地开发一套基于UDP协议的服务端程序&#xff0c;同样的也可以开发客户端&#xff0c;一般使用UDP都是使用原生的方式&#xff0c;发送消息后就不管不问&#xff0c;也就是不需要确定消息是否收到&#xff0c;这里使用Netty创建的客户端和服务端…

Windows 不同方式打开的cmd/dos窗口属性配置不同

文章目录 1. 默认值&#xff08;控制台窗口&#xff09;属性2. "C:\Windows\System32\cmd.exe" 属性3. "命令提示符"属性4. 自定义某标题cmd窗口属性5. cmd快捷方式的属性总结 最近在写某个批处理脚本时&#xff0c;意外发现 Windows系统中&#xff0c;在不…