开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【redis】 数据结构及其应用 应用1 - 分布式锁 应用

发表于 2021-11-28

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

prefix

  • 分布式锁
  • 延时队列(不如用kafka)
  • 位图(bitset减少资源占用)
  • hyperLogLog(大批量数据不绝对精确的低资源占用统计)

需要后续学习的内容

  • hyperLogLog实现原理 zhuanlan.zhihu.com/p/58519480
  • 延时队列
  • funnel(漏斗限流)

应用1 - 分布式锁

  • 本质:在redis中留个标记,其他线程后续进入的时候放弃或重试。

方案 - setnx

setnx 指令

使用setnx(set if not exists)进行占用。使用完了使用del删除。

1
2
3
4
5
csharp复制代码> setnx lock:codehole true
OK
... do something critical ...
> del lock:codehole
(integer) 1

优点:

  • 实现简单

缺点:

  • 如果出现异常导致del没有调用,出现永久占用资源的死锁。

死锁改进方案1.0 - expire

加上过期时间,即使出现异常也不会导致永久死锁。

1
2
3
4
5
6
csharp复制代码> setnx lock:codehole true
OK
> expire lock:codehole 5
... do something critical ...
> del lock:codehole
(integer) 1

缺点:

  • setnx和expire不是一条原子指令,中间出错还是变成死锁。

死锁改进方案2.0 - set的扩展参数

1
2
3
4
csharp复制代码> set lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole

使用扩展参数,讲setnx和expire变成一条原子性语句。

优点:

  • 加锁变成原子性,在执行时间之外不会出现资源永远得不到释放的结果了。

缺点:

  • 如果持有锁的线程/服务正常执行超时了,分布式锁可能会被其他的线程/服务取得,导致分布式锁失效。

超时补救

  • Redis 分布式锁不要用于较长时间的任务。

超时补救 - 随机数匹配

为value设置随机数,释放时匹配随机数是否一致。否则,只能让本线程删除。

  • 使用lua脚本,保证原子性。
1
2
3
4
5
lua复制代码if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

但是依然不能保证锁的超时的问题。

超时补救1.0 - redission看门狗

后台异步启动一个线程定时(默认10s)去检查锁是否过期(锁不需要设置过期时间,默认30s过期),如果没有过期就延长锁的过期时间,往复循环,知道当前线程释放锁看门狗才会退出

应用2 - 延时队列

//todo

应用3 - 位图

位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。

因为是字符串,因此:

  • 位层面操作:
+ 使用**setbit** 'keyname' 2 1 ,来将索引位置为2的bit设置为1.
    - 索引位置只能是**非负整数**
    - value只能为0和1
+ 使用 getbit 'keyname' 2 来获取索引位置为2的bit.
    - 不会发生越界的问题
  • 字符串层面操作:
+ 使用set,get来操作,此时和字符串操作相同
+ > 如果对应位的字节是不可打印字符,redis-cli 会显示该字符的 16 进制形式。

统计和查找

位层面下我们可以通过:

  • bitcount [start,end]统计指定范围内1的出现次数
  • bitpos [start,end] 0/1统计指定范围内第一个出现的0/1

这里我们需要注意:

  • start,end指的是字节索引,而不是bit位置的索引,因此start,end指的是从start x8到end x 8 的位置。

批量操作(好像try-redis上用不了,先了解)

可以使用 bitfield,对指定位片段进行读写。

  • 最多处理64个连续的位
  • 可以同时执行多个子指令,有三个子指令:
+ get
+ set
+ incrby

应用4 - HyperLogLog

用来解决类似:

  • UV统计(客户访问量)的存储和去重问题。
+ 有误差,但是在\*\*1%\*\*以内。
+ 节省存储空间


    - > 计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小
    - 最多只需要12k。

使用

Hyperloglog 提供了2个指令:

  • pfadd
  • pfcount

以及一个进阶指令:

  • pfmerge

指令以及用法

  • pfadd
+ pfadd 'keyname' 'value'这样子就算是往keyname里丢一个值了,如果重复了就不会增加,否则总数会加1.
  • pfcount
+ pfcount 'keyname'这个就是计数了。
  • pfmerge
+ pfmerge 'newHLLName' 'old1' 'old2'


这样子就会获得一个总和为**old1**和**old2**中不重复个数的**newHLLName**
+ pfmerge old1 old2


这样子old1中就会包括了old2中的数据,总和计数仍为不重复的

原理

概率算法,详见:zhuanlan.zhihu.com/p/58519480

应用5 - 布隆过滤器

hyperLogLog可以实现大规模数据不是特别精确的计数的需求,但是无法判断某个值是否已经存在了,布隆过滤器可以满足这部分的功能。

布隆过滤器和HyerLogLog有类似的特性:

  • 节省存储空间
  • 存在误判的可能,具体如下:
    • 判断为存在,那么key可能实际上不存在
    • 判断为不存在,那么key必然不存在
  • 不允许删除

布隆过滤器在redis4.0提供插件之后才可以使用,作为一个插件,添加到redis server中。

指令

有2个基础指令,2个批量指令,以及一个显式构造过滤器的指令:

  • bf.add - 添加元素
    • bf.add [keyname] [userName]
  • bf.exists - 查询是否存在
    • bf.exists[keyname] [userName]
  • bf.mexists - 查询多个是否存在
    • bf.mexists [keyname] [username1] [username2] […]
  • bf.madd - 添加多个
    • bf.mexists [keyname] [username] […]

布隆过滤器可以使用构造指令进行创建,降低误判的可能性:

  • bf.reserve [key] [error_rate] [initial_size]

initialSize表示预计放入的元素数量,error_rate表示在达到预计放入元素数量的大小之前,可能出现的最大的误判概率。

  • 元素实际数量超过initialSize时,误判的概率会比error_rate大。
  • error_rate越小,需要的空间越大。

原理

每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。

当一个值被放到bf中,这个值会被其中的hash函数计算出几个不同的数,放到对应的bucket中。

那么,当发生hash冲突的时候,算出的bucket对应的索引,应有的位置上就可能出现原来就有状态存在的情况,这也就解释了:

  • 当容量过大,误判概率会增加
  • 不曾进入bf的元素,也可能会被判断为存在bf中

的两个特性。

相关数的推导

k=0.7*(l/n) # 约等于

f=0.6185^(l/n) # ^ 表示次方计算,也就是 math.pow

上述这些数的含义为:

  • n:预计元素的数量
  • f:错误率
  • l:位数组长度(需要的存储空间大小,单位为bit)
  • k:输出的无偏hash函数的最佳数量

我们可以推导出:

  1. l = nlog0.6185 f
  2. k = 0.7 * log0.6185 f

这也就复合我们的预期:

  • 失误率越小
    • 需要的位数组空间越大
    • 需要的无偏hash函数越多

应用6,7 - 限流

应用6 - 简单限流

可以使用zset来实现一个简单的限流方式,算法如下:

  • 使用一个滑动窗口来维护一定时间内的访问次数
    • 我们使用zset中的score来规定时间窗口大小
    • value只需要是固定的就可以
  • 我们对于我们不关心的窗口外的数据直接删除,节约存储成本

优劣如下:

  • 优点:
    • 实现简单,不需要额外的数据结构
  • 缺点:
    • 消耗大量的存储空间

应用7 - 漏斗限流

漏斗限流是最常用的限流方法之一。

同样,在redis4.0中提供了一个限流模块 redis-cell,提供了原子限流指令。

该数据结构只有一条指令:

1
2
3
4
5
6
markdown复制代码> cl.throttle laoqian:reply 15 30 60 1
▲ ▲ ▲ ▲ ▲
| | | | └───── need 1 quota (可选参数,默认值也是1)
| | └──┴─────── 30 operations / 60 seconds 这是漏水速率
| └───────────── 15 capacity 这是漏斗容量
└─────────────────── key laoqian

应用8 - geoHash

geoHash是一个比较成熟的地理位置距离算法,redis也使用了,一般用于地理位置的计算,在如下场景中可以使用:

  • 附近的人、车等

数据结构

简单地说,geoHash可以粗略地认为是将二维平面上的数添加到了一个一维数组中。

redis中式使用zset来进行geoHash数据的存储,因此相关的维护查询操作,也可以使用zset所提供的方法来进行。

指令

redis提供的geo指令共6个:

添加

geoadd setName 经度 维度 name

可以添加多个三元组,如下:

geoadd setName 经度 维度 name 经度 维度 name1 …

至于删除,geoHash并没有geoDel的删除指令,但可以使用zrem指令来删除。

距离

使用geodist来计算两个元素之间的距离:

geodist setName name1 name2 单位

例如:

geodist company juejin ireader km

单位指的是距离单位,可以是:

m , km ,ml, ft

指的是米,千米,英里,尺

获取元素位置

既然可以存进去,那么相同的也可以取出来:

geopos setName name

例如:

geopos company juejin

    1. “116.48104995489120483”
  1. “39.99679348858259686”

这样子可以获取存进去的经纬度。而由于geohash是将二维坐标映射的是一维的一个数,因此还原出来的经纬度和存进去的数据是有较小的差别的。

获取元素hash值

geohash可以获取到经纬度编码之后的字符串:

geohash setName name

例如:

geohash company ireader

查找附近的元素

对于这个数据结构,最需要的功能就是查找附近的元素。

使用georadiusbymember,来查找该元素附近的其他元素

该指令有以下几种用法:

  1. 查找距离内的临近元素

georadiusbymember setName key distance count n asc

指的是在setName中,查找set中距离key,distance范围内,按距离正排(从近到远)n个

  • 这个指令的查询结果,会包含自身,例如:

georadiusbymember company ireader 20 km count 3 asc

这个指令可以做出对应的修改:

同样使用desc可以倒排:

georadiusbymember company ireader 20 km count 3 desc

  1. 可以带上withdist可以显示距离,带上withcoord显示经纬度,带上withhash就显示哈希值

georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc

会输出:

1
2
3
4
5
6
7
> > > bash复制代码1) 1) "ireader"
> > > 2) "0.0000"
> > > 3) (integer) 4069886008361398
> > > 4) 1) "116.5142020583152771"
> > > 2) "39.90540918662494363"
> > >
> > >
  1. 同样地,我们也可以对不在数据集中的坐标来进行类似的查询,只是将key替换成经纬度了:

georadius company 116.514202 39.905409 20 km withdist count 3 asc

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go 实践 16:接口

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

1 你将在本章学到什么?

  • 什么是类型接口?
  • 如何定义接口。
  • “实现一个接口”是什么意思?
  • 接口的优点

2 涵盖的技术概念

  • 接口 interface
  • 具体实现 concrete implementation
  • 实现一个接口
  • 接口的方法集

3 介绍

刚开始编程时,接口似乎很难理解。通常,新手程序员并不能完全理解接口的潜力。本节旨在解释什么是接口,它的有趣之处在哪里,以及如何创建接口。

4 接口的基本定义

  • 接口是定义一组行为的契约
  • 接口是一个纯粹的设计对象,它们只是定义了一组行为(即方法),而没有给出这些行为的任何实现。
  • 接口是一种类型,它定义了一组方法而不实现它们

“实现” = “编写方法的代码”,这是一个示例接口类型(来自标准包 io):

1
2
3
go复制代码type Reader interface {
Read(p []byte) (n int, err error)
}

这里我们有一个名为 Reader 的接口类型,它指定了一种名为 Read 的方法。该方法没有具体实现,唯一指定的是方法名称及其签名(参数类型和结果类型)。

4.0.0.1 接口类型的零值

接口类型的零值为 nil,例子:

1
2
3
go复制代码var r io.Reader
log.Println(r)
// 2021/11/28 12:27:52 <nil>

5 基本示例

1
2
3
4
5
6
7
8
9
10
11
go复制代码type Human struct {
Firstname string
Lastname string
Age int
Country string
}

type DomesticAnimal interface {
ReceiveAffection(from Human)
GiveAffection(to Human)
}
  • 首先,我们声明一个名为 Human 的类型
  • 我们声明了一个名为 DomesticAnimal 的新类型接口
  • 这种类型的接口有一个由两个方法组成的方法集:ReceiveAffection 和 GiveAffect。

DomesticAnimal 是一个契约。

  • 它告诉开发者,要成为 DomesticAnimal,我们至少需要有两种行为:ReceiveAffection 和 GiveAffection

让我们创建两个类型:

1
2
3
4
5
6
7
go复制代码type Cat struct {
Name string
}

type Dog struct {
Name string
}

我们有两种新类型。为了让他们遵守我们的接口 DomesticAnimal 的契约,
我们必须为每种类型定义接口指定的方法。

我们从 Cat 类型开始:

1
2
3
4
5
6
7
go复制代码func (c Cat) ReceiveAffection(from Human) {
fmt.Printf("The cat named %s has received affection from Human named %s\n", c.Name, from.Firstname)
}

func (c Cat) GiveAffection(to Human) {
fmt.Printf("The cat named %s has given affection to Human named %s\n", c.Name, to.Firstname)
}

现在 Cat 类型实现了 DomesticAnimal 接口。我们现在对 Dog 类型做同样的事情:

1
2
3
4
5
6
7
go复制代码func (d Dog) ReceiveAffection(from Human) {
fmt.Printf("The dog named %s has received affection from Human named %s\n", d.Name, from.Firstname)
}

func (d Dog) GiveAffection(to Human) {
fmt.Printf("The dog named %s has given affection to Human named %s\n", d.Name, to.Firstname)
}

我们的 Dog 类型现在正确地实现了 DomesticAnimal 接口。现在我们可以创建一个函数,它接受一个带有参数的接口:

1
2
3
4
go复制代码func Pet(animal DomesticAnimal, human Human) {
animal.GiveAffection(human)
animal.ReceiveAffection(human)
}

Pet 函数将 DomesticAnimal 类型的接口作为第一个参数,将 Human 作为第二个参数。

在函数内部,我们调用了接口的两个函数。

让我们使用这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码func main() {

// Create the Human
var john Human
john.Firstname = "John"


// Create a Cat
var c Cat
c.Name = "Maru"

// then a dog
var d Dog
d.Name = "Medor"

Pet(c, john)
Pet(d,john)
}
  • Dog 和 Cat 类型实现了接口 DomesticAnimal 的方法
  • 也就是说 Dog 和 Cat 类型的任何变量都可以看作是 DomesticAnimal

只要 Cat 实现的方法的函数签名与接口定义一致就可以,不强制要求完全相同变量名和返回名。所以我们将函数 func (c Cat) ReceiveAffection(from Human) {...} 改成 func (c Cat) ReceiveAffection(f Human) {...} 也是可以的

6 编译器在看着你!

遵守类型 T 的接口契约意味着实现接口的所有方法。让我们试着欺骗编译器看看会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// ...
// let's create a concrete type Snake
type Snake struct {
Name string
}
// we do not implement the methods ReceiveAffection and GiveAffection intentionally
//...


func main(){

var snake Snake
snake.Name = "Joe"

Pet(snake, john)
}
  • 我们创建了一个新类型的 Snake
  • 该类型没有实现 DomesticAnimal 动物的任何方法
  • 在主函数中,我们创建了一个新的 Snake 类型的变量
  • 然后我们用这个变量作为第一个参数调用 Pet 函数

结果是编译失败:

1
2
shell复制代码./main.go:70:5: cannot use snake (type Snake) as type DomesticAnimal in argument to Pet:
Snake does not implement DomesticAnimal (missing GiveAffection method)

编译器在未实现的按字母顺序排列的第一个方法处检查停止。

7 例子:database/sql/driver.Driver

我们来看看 Driver 接口(来自包database/sql/driver)

1
2
3
go复制代码type Driver interface {
Open(name string) (Conn, error)
}
  • 存在不同种类的 SQL 数据库,因此 Open 方法有多种实现。
  • 为什么?因为你不会使用相同的代码来启动到 MySQL 数据库和 Oracle 数据库的连接。
  • 通过构建接口,你可以定义一个可供多个实现使用的契约。

8 接口嵌入

你可以将接口嵌入到其他接口中。让我们举个例子:

1
2
3
4
5
6
7
8
9
10
11
go复制代码// the Stringer type interface from the standard library
type Stringer interface {
String() string
}
// A homemade interface
type DomesticAnimal interface {
ReceiveAffection(from Human)
GiveAffection(to Human)
// embed the interface Stringer into the DomesticAnimal interface
Stringer
}

在上面的代码中,我们将接口 Stringer 嵌入到接口 DomesticAnimal 中。
因此,已经实现了 DomesticAnimal 的其他类型必须实现 Stringer 接口的方法。

  • 通过接口嵌入,你可以在不重复的情况下向接口添加功能。
  • 这也是有代价的,如果你从另一个模块嵌入一个接口,你的代码将与其耦合
    • 其他模块接口的更改将迫使你重写代码。
    • 请注意,如果依赖模块遵循语义版本控制方案,则这种危险会得到缓和
    • 你可以毫无畏惧地使用标准库中的接口

9 来自标准库的一些有用(和著名)的接口

9.1 Error 接口

1
2
3
go复制代码type error interface {
Error() string
}

这个接口类型被大量使用,用于当函数或方法执行失败是返会error类型接口:

1
2
3
go复制代码func (c *Communicator) SendEmailAsynchronously(email *Email) error {
//...
}

要创建一个 error ,我们通常调用: fmt.Errorf() 返回一个 error 类型的结果,或者使用 errors.New()函数。
当然,你也可以创建实现error接口的类型。

9.2 fmt.Stringer 接口

1
2
3
go复制代码type Stringer interface {
String() string
}

使用 Stringer 接口,你可以定义在调用打印方法时如何将类型打印为字符串(fmt.Errorf(),fmt.Println, fmt.Printf, fmt.Sprintf…)

这有一个示例实现

1
2
3
4
5
6
7
8
9
10
go复制代码type Human struct {
Firstname string
Lastname string
Age int
Country string
}

func (h Human) String() string {
return fmt.Sprintf("human named %s %s of age %d living in %s",h.Firstname,h.Lastname,h.Age,h.Country)
}

Human 现在实现了 Stringer 接口:

1
2
3
4
5
6
7
8
9
10
11
go复制代码package main

func main() {
var john Human
john.Firstname = "John"
john.Lastname = "Doe"
john.Country = "USA"
john.Age = 45

fmt.Println(john)
}

输出:

1
shell复制代码human named John Doe of age 45 living in the USA

9.3 sort.Interface 接口

1
2
3
4
5
go复制代码type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

通过在一个类型上实现 sort.Interface 接口,可以对一个类型的元素进行排序(通常,底层类型是一个切片)。

这是一个示例用法(来源:sort/example_interface_test.go):

1
2
3
4
5
6
7
8
9
10
11
go复制代码type Person struct {
Name string
Age int
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person

func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
  • ByAge 类型实现了 sort.Interface
    • 底层类型是 Person 的一个切片
  • 接口由三个方法组成:
    • Len() int:返回集合内的元素数
    • Less(i, j int) bool:如果索引 i 处的元素应该排在索引 j 处的元素之前,则返回 true
    • Swap(i, j int):交换索引 i & j 处的元素;换句话说,我们应该将位于索引 j 的元素放在索引 i 处,而位于索引 i 的元素应该放在索引 j 处。
      然后我们可以使用 sort.Sort 函数对 ByAge 类型的变量进行排序
1
2
3
4
5
6
7
8
9
10
go复制代码func main() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
}

sort.Sort(ByAge(people))
}

10 隐式实现

接口是隐式实现的。当你声明一个类型时,你不必指定它实现了哪些接口。

11 PHP 和 JAVA

在其他语言中,你必须指定接口实现。
这是 Java 中的一个示例:

1
2
3
4
5
6
7
8
9
java复制代码// JAVA
public class Cat implements DomesticAnimal{
public void receiveAffection(){
//...
}
public void giveAffection(){
//..
}
}

这是 PHP 中的另一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
PHP复制代码//PHP
<?php

class Cat implements DomesticAnimal {
public function receiveAffection():void {
// ...
}
public function giveAffection():void {
// ...
}
}
?>

你可以看到,在声明实现接口的类时,必须添加关键字"implements"。

你可能会问 Go 运行时如何处理这些隐式接口实现。我们将后面解释接口值的机制。

12 空接口

Go 的空接口是你可以编写的最简单、体积更小的接口。它的方法集正好由 0 个方法组成。

1
go复制代码interface{}

也就是说,每种类型都实现了空接口。你可能会问为什么需要这么无聊的空接口。根据定义,空接口值可以保存任何类型的值。如果你想构建一个接受任何类型的方法,它会很有用。
让我们从标准库中举一些例子。

  • 在 log 包中,你有一个 Fatal 方法,可以将任何类型的输入变量作为输入:
1
go复制代码func (l *Logger) Fatal(v ...interface{}) { }
  • 在 fmt 包中,我们还有许多方法将空接口作为输入。例如 Printf 函数:
1
go复制代码func Printf(format string, a ...interface{}) (n int, err error) { }

12.1 类型转换

接受空接口作为参数的函数通常需要知道其输入参数的有效类型。
为此,该函数可以使用“类型开关”,这是一个 switch case 将比较类型而不是值。
这是从标准库(文件 runtime/error.go,包 runtime)中获取的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
go复制代码// printany prints an argument passed to panic.
// If panic is called with a value that has a String or Error method,
// it has already been converted into a string by preprintpanics.
func printany(i interface{}) {
switch v := i.(type) {
case nil:
print("nil")
case bool:
print(v)
case int:
print(v)
case int8:
print(v)
case int16:
print(v)
case int32:
print(v)
case int64:
print(v)
case uint:
print(v)
case uint8:
print(v)
case uint16:
print(v)
case uint32:
print(v)
case uint64:
print(v)
case uintptr:
print(v)
case float32:
print(v)
case float64:
print(v)
case complex64:
print(v)
case complex128:
print(v)
case string:
print(v)
default:
printanycustomtype(i)
}
}

image

12.2 关于空接口的使用

  • 你应该非常小心地使用空接口。
  • 当你别无选择时,请使用空接口。
  • 空接口不会向将使用你的函数或方法的人提供任何信息,因此他们将不得不参考文档,这可能会令人沮丧。

你更喜欢哪种方法?

1
2
3
4
5
6
7
go复制代码func (c Cart) ApplyCoupon(coupon Coupon) error  {
//...
}

func (c Cart) ApplyCoupon2(coupon interface{}) (interface{},interface{}) {
//...
}

ApplyCoupon 方法严格指定它将接受和返回的类型。而 ApplyCoupon2 没有在输入和输出中指定它的类型。作为调用方,ApplyCoupon2 的使用难度比 ApplyCoupon 大。

13 实际应用:购物车存储

13.1 规则说明

你建立了一个电子商务网站;你必须存储和检索客户购物车。必须支持以下两种行为:

  1. 通过 ID 获取购物车
  2. 将购物车数据放入数据库

为这两种行为提出一个接口。还要创建一个实现这两个接口的类型(不要实现方法中的逻辑)。

13.2 答案

这是一个设计的接口:

1
2
3
4
go复制代码type CartStore interface {
GetById(ID string) (*cart.Cart, error)
Put(cart *cart.Cart) (*cart.Cart, error)
}

实现接口的类型:

1
2
3
4
5
6
7
8
9
go复制代码type CartStoreMySQL struct{}

func (c *CartStoreMySQL) GetById(ID string) (*cart.Cart, error) {
// implement me
}

func (c *CartStoreMySQL) Put(cart *cart.Cart) (*cart.Cart, error) {
// implement me
}

另一种实现接口的类型:

1
2
3
4
5
6
7
8
9
go复制代码type CartStorePostgres struct{}

func (c *CartStorePostgres) GetById(ID string) (*cart.Cart, error) {
// implement me
}

func (c *CartStorePostgres) Put(cart *cart.Cart) (*cart.Cart, error) {
// implement me
}
  • 你可以为你使用的每个数据库模型创建一个特定的实现
  • 添加对新数据库引擎的支持很容易!你只需要创建一个实现接口的新类型。

14 为什么要使用接口?

14.1 易于升级

当你在方法或函数中使用接口作为输入时,你将程序设计为易于升级的。未来的开发人员(或未来的你)可以在不更改大部分代码的情况下创建新的实现。

假设你构建了一个执行数据库读取、插入和更新的应用程序。你可以使用两种设计方法:

  1. 创建与你现在使用的数据库引擎密切相关的类型和方法。
  2. 创建一个接口,列出数据库引擎的所有操作和具体实现。
  • 在第一种方法中,你创建将特定实现作为参数的方法。
  • 通过这样做,你将程序限制到一个实现。
  • 在第二种方法中,你创建接受接口的方法。
  • 改变实现就像创建一个实现接口的新类型一样简单。

14.2 提高团队合作

团队也可以从接口中受益。
在构建功能时,通常需要多个开发人员来完成这项工作。如果工作需要两个团队编写的代码进行交互,他们可以就一个或多个接口达成一致。
然后,两组开发人员可以处理他们的代码并使用商定的接口。他们甚至可以 mock 其他团队的返回结果。通过这样做,团队不会被阻塞。

14.3 Benefit from a set of routines

在自定义类型上实现接口时,你可以不需要开发就使用的附加功能。让我们从标准库中举一个例子:sort 包。这并不奇怪。这个包是用来进行排序的。这是 go 源代码的摘录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码// go v.1.10.1
package sort
//..

type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}

// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
n := data.Len()
quickSort(data, 0, n, maxDepth(n))
}

在第一行,我们声明当前包:sort。在接下来的几行中,程序员声明了一个名为 Interface 的接口。这个接口 Interface 指定了三个方法:Len、Less、Swap。

在接下来的几行中,函数 Sort 被声明。它将接口类型 data 作为参数。这是一个非常有用的函数,可以对给定的数据进行排序。

我们如何在我们的一种类型上使用这个函数?实现接口

假设你有一个 User 类型:

1
2
3
4
5
go复制代码type User struct {
firstname string
lastname string
totalTurnover float64
}

还有一个类型 Users ,它是 User 类型切片:

1
go复制代码type Users []User

让我们创建一个 Users 实例并用三个 User 类型的变量填充它:

1
2
3
4
5
6
7
8
go复制代码user0 := User{firstname:"John", lastname:"Doe", totalTurnover:1000}
user1 := User{firstname:"Dany", lastname:"Boyu", totalTurnover:20000}
user2 := User{firstname:"Elisa", lastname:"Smith Brown", totalTurnover:70}

users := make([]Users,3)
users[0] = user0
users[1] = user1
users[2] = user2

如果我们想按营业额排序怎么办?我们可以从头开始开发符合我们规范的排序算法。或者我们可以只实现使用 sort 包中的内置函数.Sort 所需的接口。我们开始吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// Compute the length of the array. Easy...
func (users Users) Len() int {
return len(users)
}

// decide which instance is bigger than the other one
func (users Users) Less(i, j int) bool {
return users[i].totalTurnover < users[j].totalTurnover
}

// swap two elements of the array
func (users Users) Swap(i, j int) {
users[i], users[j] = users[j], users[i]
}

通过声明这些函数,我们可以简单地使用 Sort 函数:

1
2
3
4
go复制代码sort.Sort(users)
fmt.Println(users)
// will output :
[{Elisa Smith Brown 70} {John Doe 1000} {Dany Boyu 20000}]

15 一点建议

  1. 尽量使用标准库提供的接口
  2. 方法太多的接口很难实现(因为它需要编写很多方法)。

16 随堂测试

16.1 问题

  1. 举一个接口嵌入另一个接口的例子。
  2. 判断真假。嵌入接口中指定的方法不是接口方法集的一部分。
  3. 说出使用接口的两个优点。
  4. 接口类型的零值是多少?

16.2 答案

  1. 举一个接口嵌入另一个接口的例子。
1
2
3
4
go复制代码type ReadWriter interface {
Reader
Writer
}
  1. 判断真假。嵌入接口中指定的方法不是接口方法集的一部分。
    错。接口的方法集是由两个部分组成:
    1. 直接指定到接口中的方法
    2. 来自嵌入接口的方法
  2. 说出使用接口的两个优点。
    1. 轻松地在开发人员之间拆分工作:
    2. 定义接口类型
    3. 一个人开发接口的实现
    4. 另一个人可以在其功能中使用接口类型
    5. 两个人可以互不干扰地工作。
    6. 易于升级
    7. 当你创建一个接口时,你就创建了一个契约
    8. 不同的实现可以履行这个契约。
    9. 在一个项目的开始,通常有一个实现
    10. 随着时间的推移,可能需要另一种实现方式。
  3. 接口类型的零值是多少?
    nil

17 关键要点

  • 接口就是契约
  • 它指定方法(行为)而不实现它们。
1
2
3
4
go复制代码type Cart interface {
GetById(ID string) (*cart.Cart, error)
Put(cart *cart.Cart) (*cart.Cart, error)
}
  • 接口是一种类型(就像structs, arrays, maps,等)
  • 我们将接口中指定的方法称为接口的方法集。
  • 一个类型可以实现多个接口。
  • 无需明确类型实现了哪个接口
    • 与其他需要声明它的语言(PHP、Java 等)相反
  • 一个接口可能嵌入到另一个接口中;在这种情况下,嵌入的接口方法被添加到接口中。
  • 接口类型可以像任何其他类型一样使用
  • 接口类型的零值为 nil。
  • 任何类型实现空接口 interface{}
  • 空接口指定了 0 个方法
  • 要获取空接口的具体类型,您可以使用 type switch:
1
2
3
4
5
6
7
8
go复制代码switch v := i.(type) {
case nil:
print("nil")
case bool:
print(v)
case int:
print(v)
}
  • 当我们可以通过各种方式实现一个行为时,我们或许可以创建一个接口。
    • 例如:存储(我们可以使用 MySQL、Postgres、DynamoDB、Redis 数据库来存储相同的数据)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我的docker随笔37:使用gitlab和jenkins实

发表于 2021-11-28

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

本文涉及一种利用容器部署 gitlab 和 jenkins 服务实现持续集成(CICD)的方法,其目的是为了在实际工作中使用代码托管及自动化操作。

一、引言

因工作需要,需部署 gitlab 和 jenkins 服务器进行 CICD 测试,换个高大上的名称,叫“组织革新”。本文记录个人的实践,但不涉及部署的具体步骤。

二、技术小结

  • 不同工程,配置不同,本文使用 C++ 工程为例进行实验。
  • 在配置 jenkins 时,建议经常使用页面下方的“应用”,随时保存设置好的参数,以防不测。

三、gitlab和jenkins联调

3.1 概述

实现CICD分几个阶段:使用 gitlab 托管代码,使用 jenkins 进行编译、打包以及发布。两个服务器各有项目对应,同时要进行必要的配置。不同项目,配置方式不尽相同。本节的配置原则上按顺序进行,因此会在 gitlab 和 jenkins 之间来回切换,故先行说明。

3.2 前置条件

3.2.1 允许本地网络请求

使用 root 用户登录 gitlab 服务器,在管理员配置选项选择网络(Setting) 页面,在外发请求(Outbound requests) 中,选择“允许Webhook和服务对本地网络的请求(Allow requests to the local network from web hooks and services) ”,保存。如图1所示。

进行该设置的目的是因为本文的 gitlab 和 jenkins 服务均在同一物理服务器上使用 docker 部署。如果不设置,则在 gitlab 中设置 webhooks 时会提示Url is blocked: Requests to the local network are not allowed。

1.png

说明:如果jenkins和gitlab不在同一服务器,则不需要进行此设置。

3.2.2 关闭 CSFR

进入 jenkins 容器,找到/usr/local/bin/jenkins.sh文件,找到 exec java 行,在-Duser.home="$JENKINS_HOME"后添加-Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true,完整的一行语句如下:

1
bash复制代码exec java -Duser.home="$JENKINS_HOME" -Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true "${java_opts_array[@]}" -jar ${JENKINS_WAR} "${jenkins_opts_array[@]}" "$@

说明:在笔者定制的 jenkins 镜像中已经进行该修改了。

进行该设置,是因为高版本 Jenkins 无法在界面关闭跨站请求伪造保护(CSRF),因此在 gitlab 进行 webhooks 时会认证失败。提示:

1
复制代码 Hook executed successfully but returned HTTP 403

关闭之后,再次进入 CSFR 页面,提示:

1
vbnet复制代码This configuration is unavailable because the System property hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION is set to true.

image-20211020161017526.png

说明修改成功。

对于物理机部署的 jenkins,则在 /etc/sysconfig/jenkins 文件中找到 JENKINS_JAVA_OPTIONS, 设置如下:

JENKINS_JAVA_OPTIONS=”-Djava.awt.headless=true -Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true”

3.3 准备 gitlab 仓库

本文使用仓库地址为 http://10.8.18.168:8888/latelee/ci_test。

3.4 配置 jenkins 项目

3.4.1 新建项目

在 jenkins 首页左侧,点击“新建任务”,输入项目项目,选择第一项“自由风格软件项目”。
2.png

点击“确定”进入配置页面。

3.4.2 指定git仓库

在源码管理选项页面中,输入 gitlab 仓库地址,注意,URL 地址后须添加.git后缀。

3.png

点击“添加”->Jenkins,添加凭据,默认类型为用户名和密码。

4.png

添加后,选择添加的凭据,红色错误提示消失。

5.png

3.4.3 设置触发条件

在构建触发器页面进行触发器的选择。此处有多种方式可选,如定时构建和指定 gitlab 方式。

6.png

勾选“Build when a change is pushed to GitLab.”。注意,该项后面的 URL 地址需要记住,将在 gitlab 中使用。点击“高级”,在“Secret token”处点击“Generate”,生成 token,该 token 亦需要记住。

7.png

进行此设置目的是,当提交代码到 gitlab 仓库时,会自动触发 jenkins 执行 一次构建。触发条件在对应 gitlab 仓库中进行设置。如果不需要自动触发,则可以不触发条件。

3.4.4 指定构建步骤

在构建页面,点击“增加构建步骤”,选择“执行shell”,输入构建的命令。

8.png

3.4.5 指定构建后的步骤

在构建后操作页面,点击“增加构建后操作步骤”。选择“send build artifacts over SSH”。选择服务器、源目录、目标目录及执行的命令。注意,在本文实践前已经设置好服务器及目标,故只指定源文件和执行命令即可。

9.png

在笔者实践中发现,在“构建环境”、“构建”、“构建后操作”三个页面均可以选择将文件通过 SSH 发送服务器。

3.5 设置 gitlab 的 webhooks

在 gitlab 项目http://10.8.18.168:8888/latelee/ci_test页面, 选择“Settings”->“Webhooks”,输入上小节生成的 URL 和 token。

10.png

点击页面下方“Add webhook”,添加。

11.png

注:同一个gitlab仓库,可支持多个 webhook。

点击“Test”,选择“Push events”,进行测试验证。为保证触发成功,强烈建议在此处先行测试。

12.png

触发类型有很多种,根据实际情况选择。

3.4 验证

提交代码到 gitlab 仓库(此处从略)。稍等片刻,在 jenkins 工程的看到进行了触发。

13.png

输出日志如下:

14.png

编译、运行、执行ssh远程服务均成功。在远程服务器的/tmp目录出现a.out和log.txt。

四、其它的配置

4.1 邮件通知

jenkins系统设置。见前面文章。此处关注项目配置。

注意,邮件通知需要根据不同应用情况设置,此处作为示例,只关注是否能在构建时发邮件。至于何时何条件触发,非本文范围。

定位到构建后操作页面,添加Editable Email Notification。

15.png

在该插件配置处选择高级,在“Always”处输入接收者邮件,其它保持不变(即使用默认的模板)。

16.png

保存即可。

4.2 未登录时可查看构建信息

进入“系统管理”->“全局安全配置”页面,勾选“匿名用户具有可读权限”。如下:

17.png

此时,无须登录,即可查看项目的构建构建,可根据情况决定是否勾选。

五、进阶

构建的工程位于/var/lib/jenkins/workspace/,可以先在工程目录进行测试,再使用 Jenkins 进行集成。

六、问题

在构建阶段直接启动bash执行脚本,能启动,但随后被jenkins停止。终端提示:

1
arduino复制代码Process leaked file descriptors. See https://www.jenkins.io/redirect/troubleshooting/process-leaked-file-descriptors for more information

官方解释:

1
css复制代码To reliably kill processes spawned by a job during a build, Jenkins contains a bit of native code to list up such processes and kill them.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HDFS概述

发表于 2021-11-28

HDFS是以流式数据访问的模式来存储超大文件的一个文件系统,运行与集群上。

  • 流式数据访问:HDFS的构建思路是这样的:一次写入、多次读取是比较高效的访问模式。
  • 运行与商用的硬件中:商用的硬件在生产环境中出现故障的概率是比较大的。HDFS对数据进行数据备份,一般来讲,一份存储在HDFS集群中数据会有3份副本存储在不同的集群节点上,保证了单点数据损失的问题,HDFS保证了在集群中其中的节点发生故障后仍然可以稳定的运行,用户丝毫没有察觉。
  • 不适合低时延的数据访问:HDFS是为高数据吞吐量设计的,会以提高数据的访问时间延迟为代价。
  • 不适合大量小文件的场景:由于NameNode是将文件的元数据信息存储在内存中,大量的小文件会大大降低内存的利用率。
  • 只能文件内容追加:HDFS中的文件写入只支持单个写入,而且只能够以添加的方式写入。

HDFS-数据块

HDFS中的数据是按照块进行存储的,默认的块大小为128MB,这个块大小比一般的磁盘以及以及一些本地的文件系统的块大小要大得多,这是为什么呢?

HDFS的块大小比较大的目的是为了能够尽可能地减小寻址的开销。

假设一个12800MB的文件按照两种方式来分块:(寻址时间固定为:10ms;数据读取速度为100MB/s)

  1. 块大小为1MB->分成12800个数据块,总共需要的时间为:

10ms*12800 + 12800MB/(100MB/s) = 128s + 128s = 256s
2. 块大小为128MB->分成100个数据块,总共需要的时间为:

10ms*100 + 1280MB/(100MB/s) = 1s + 128s = 129s

一般来讲,HDFS的数据块大小为128MB或者256MB,如果太大了,会导致程序在处理这块数据的时候,会非常慢,一直等待数据传输。

为什么要进行分块存储?

  1. HDFS面向的是大数据,数据文件通常比较大,可能会大于集群中单节点的磁盘容量,采用数据分块可以将大文件分成不同的数据块存放在不同的节点上
  2. 块大小是固定的,因此每个节点的容量可以用块的个数来衡量,另外块数据和元数据分块,类似于权限信息可以不和数据本身一起存储,只用存放在元数据中即可。

NameNode and DataNode

NameNode:用于存储文件的元数据(meta data),维护某个文件对应的文件块以及其所在的位置,同时还维护着整个集群的文件目录树结构。

DataNode:实际存储数据的地方。

由此可见, 当NameNode宕机之后,整个集群的数据将会丢失,因为文件数据时混乱地存放在集群中的,因此对NameNode实现容错机制非常重要,Hadoop提供两种机制:

  1. 备份元数据信息到本地或者网络文件系统,将内存中元数据信息持久化到磁盘中或者其他的网络文件系统中,在将元数据写入到本地磁盘的同时,写入到一个网络文件系统中,以备后续的数据恢复。
  2. 运行一个辅助的NameNode节点,该节点和NameNode节点不运行在同一个机器上,辅助NameNode会定期的合并NameNode的编辑日志以及命名空间镜像,当NameNode发生故障宕机时,可以临时使用辅助NameNode代替NameNode,但是这样会有数据的丢失,辅助NameNode 的数据时滞后于NameNode的。

Java client API

Hadoop是使用Java编写的,所以原生支持Java的API.

我提前搭建好了Hadoop集群

模板代码(JUnit)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class HDFSDemo {

private FileSystem fs;

final String HADOOP_URI_ADDRESS = "hdfs://192.168.10.102:8020";

final String HADOOP_USER = "root";

@Before
public void init() throws URISyntaxException, IOException, InterruptedException {
// 获取文件系统
fs = FileSystem.get(new URI(HADOOP_URI_ADDRESS),
new Configuration(),
HADOOP_USER);
}

@After
public void after() throws IOException {
// 关闭资源
fs.close();
}
}

文件下载

1
2
3
4
5
6
7
java复制代码@Test
public void downloadFile() throws IOException {
fs.copyToLocalFile(false,//是否删除源文件
new Path("/input/word.txt"),//hdfs中的文件地址
new Path("d:/word_down.txt"),//本地的文件地址
true);//是否开启校验
}

文件上传

1
2
3
4
java复制代码@Test
public void uploadFile() throws IOException {
fs.copyFromLocalFile(new Path("d:/word_down.txt"), new Path("/input/"));
}

HDFS提供很多Java的API,这里不再做过多的展示。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java手写一个RPC框架(Spring-boot-star

发表于 2021-11-28

Java基于Netty/Zookeeper实现的RPC框架

基于Spring Boot Starter的小型RPC框架。编写这个RPC框架并不是为了重复造轮子,而是出于学习的目的,通过手写一款RPC框架来达到知识的学习和应用目的。简易的RPC框架(danran-rpc),底层使用Netty进行网络通信,使用Zookeeper为注册中心。该项目可以Maven打包直接加入其他项目运行。另外一个仓库:

  • https://gitee.com/lengdanran/danran-rpc-debug
  • https://github.com/lengdanran/danran-rpc-debug

为该项目的整合调试仓库,在里面可调试danran-rpc的源码

快速上手

示例代码:gitee.com/lengdanran/…

  • https://gitee.com/lengdanran/danran-rpc.git
  • https://github.com/lengdanran/danran-rpc.git

生成本地Maven依赖包

从以上的git仓库地址clone代码到本地,然后进入到项目pom目录中,执行maven安装命令:

1
shell复制代码mvn clean install

服务提供者-消费者同时引入该maven依赖

引入打包安装好的maven依赖,因为该RPC框架内部使用了和Springboot本身冲突的日志框架,引入依赖时,最好将danran-rpc的日志依赖移除,避免程序无法启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<dependency>
<groupId>danran.rpc</groupId>
<artifactId>danran-rpc-spring-boot-starter</artifactId>
<version>1.0.1</version>
<!--排除日志冲突-->
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

服务提供者、消费者同时配置注册中心(请更换为自己的ZK地址)

在项目的application.properties添加如下配置

1
2
3
properties复制代码danran.rpc.register-address=121.4.195.203:2181 # zk的地址
danran.rpc.protocol=danran
danran.rpc.server-port=6666

服务提供者使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码package provider.service;

import common.api.BookService;
import common.entity.Book;
import danran.rpc.annotation.RPCService;
import org.springframework.beans.factory.annotation.Autowired;
import provider.mapper.BookMapper;

import java.util.List;

/**
* @Classname BookServiceImpl
* @Description TODO
* @Date 2021/8/24 15:50
* @Created by ASUS
*/
@RPCService
public class BookServiceImpl implements BookService {

@Autowired
private BookMapper bookMapper;

/**
* @return 所有的书籍信息
*/
@Override
public List<Book> getAllBooks() {
return bookMapper.getAllBooks();
}
}

在具体的服务实现类上添加@RPCService注解,即可将该类作为服务提供者注册到zookeeper中,消费端可以发现该服务,添加该注解之后,将会作为Spring的Component注入到Spring的容器中。(不再需要添加@Component注解)

服务消费者示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码package consumer.controller;

import common.api.BookService;
import common.entity.Book;
import danran.rpc.annotation.InjectService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* @Classname ConsumerController
* @Description TODO
* @Date 2021/8/24 19:04
* @Created by ASUS
*/
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

@InjectService
private BookService bookService;

@GetMapping("/get_all_books")
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
}

使用@InjectService注解,可以启动服务发现,然后自动注入远程服务的代理对象。

示例运行结果

启动两个Springboot的web服务,provider提供查询数据库的具体实现,consumer远程调用该服务。

  • provider启动日志

  • consumer启动日志

启动postman接口调试工具,访问服务接口:

接口成功返回数据,查看程序日志:

RPC原理

定义

RPC(Remote procedure call)远程过程调用,简单理解是本地需要某个服务,而具体的服务由另外一个独立的服务端提供,我们可以通过网络等其他方式通知到服务端执行对应的服务,然后返回我们关心的信息。

RPC:打电话查成绩

本地:你

远端服务:你孩子的班主任

现在你想了解孩子的期末考试成绩,而成绩在班主任那里,此时你便打电话给班主任,然后班主任查询,告诉你孩子的成绩是多少,这就是一个RPC过程。

电脑1想要调用电脑2中的某个服务,会发出一个RPC call给通信中间件,通信中间件将请求编组后通过网络将请求发送到电脑2端的通信中间件,通信中间件从网络中读取请求数据包,解组,然后在电脑2内部发起本地调用,调用结束之后,会得到一个返回数据,通信中间件再将响应数据编组通过网络发送到电脑1。

此时会有一个问题:电脑1(消费端)如何知道远程服务的地址?

服务发现

解决这个问题,可以采用第三方,即是这个人专门来管理服务,一旦有服务请求来,就告诉对方你所请求的服务的地址有哪些。

因此,目前比较流行的RPC框架Dubbo采用的是如下的架构:

主要有消费者,注册中心,提供者三方架构角色,消费者通过注册中心去订阅自己关心的服务,注册中心会将注册了的服务地址等相关信息返回给消费者,消费者再通过具体的协议将数据发送到socket中最后由网卡发送到网络上,最后得到服务提供者的响应数据。

danran-rpc实现

文件结构

  • annotation:里面为InjectService和RPCService两个注解的定义,用来注入和发布服务
  • client:为客户端服务发现、服务代理以及网络通信的实现
  • common:框架的公共模块,包含协议、序列化以及公用Entity的定义
  • config:Spring容器启动时的自动配置类
  • properties:用户自定义参数
  • server:rpc服务的注册、发布与启动

实现组件

danran-rpc的实现是参照着dubbo实现的,主要采用了如下几个组件:

  • 网络通信——Netty
  • 注册中心——Zookeeper(后续应该会继续实现不同的注册中心:Redis、Nacos、Eureka等)
  • 代理——Java动态代理
  • IOC——Spring

整体架构

启动流程

Client包实现

依据RPC的原理,客户端需要将请求通过网络通信中间件将数据编组发送出去并得到对应的响应数据。

NetClient

该接口定义了网络请求客户端,定义网络的请求规范,可以由不同的网络通信框架来实现,用户可以自定义实现类,可以整合到danran-rpc框架中。这里采用Netty实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public byte[] sendRequest(byte[] data, Service service) throws InterruptedException {
String[] ip_port = service.getAddress().split(":");
String ip = ip_port[0];
int port = Integer.parseInt(ip_port[1]);

SendHandler sendHandler = new SendHandler(data);
byte[] rsp;
// 配置客户端
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
// 初始化通道
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(sendHandler);
}
});
// 启动连接
bootstrap.connect(ip, port).sync();
rsp = (byte[]) sendHandler.rspData();
logger.info("Send Request and get the response: {}", rsp);
} finally {
// 释放线程组资源
group.shutdownGracefully();
}
return rsp;
}

ClientProxyFactory

这是客户端代理工厂, 用于创建远程服务的代理类,同时封装了请求和响应数据的编组合解组操作。该工厂类包含了服务发现实例对象以及具体的网络层实现,可以同服务发现实例去发现服务以及发送数据。里面的getProxy(Class<T> clazz)方法会返回clazz的代理对象。核心代码为:

1
2
3
4
5
6
java复制代码return (T) this.objectCache.computeIfAbsent(
clazz,
cls -> newProxyInstance(
cls.getClassLoader(),
new Class<?>[]{cls},
new ClientInvocationHandler(cls)));

该段代码会以该clazz的Class来构建一个代理对象,具体的代理实现为ClientInvocationHandler.利用的是JDK的动态代理,实现了InvocationHandler接口。

其中关键为invoke()方法的实现。通过反射的方式调用被代理对象的方法。

首先它会先通过被代理对象clazz获得被代理对象的全类名,服务发现实例会以获得的全类名去服务注册中心去发现与之对应的远程服务。

1
java复制代码List<Service> services = serviceDiscovery.getServices(serviceName);

Service中包含了远程服务的名称、服务协议以及服务地址(ip+端口)获得具体远程服务之后,会生成一个RequestWrap请求包装类,会将服务名称,被代理的方法名称,被代理方法的参数类型以及被代理方法的参数封装起来。之后便会根据协议去编组该封装请求得到data,最后通过网络层NetClient实现请求发送。最后会从网络层实现那里获取得到响应数据,解组响应得到ResponseWrap,从响应封装中得到具体的响应数据。

服务发现——ServiceDiscovery

服务发现会从注册中心查询得到远程服务注册的信息,然后返回,在danran-rpc中,目前是以Zookeeper为注册中心实现的,故具体的实现类为ZkDiscovery.以下是服务发现具体实现流程:

服务发现的依据和服务注册的依据是完全一致的,确保注册了的服务一定会被发现。

  • 根据提供的服务名称(默认是全类名)拼接znode路径
  • 从Zookeeper中获得该路径下的所有子节点的名称(子节点的名称即为具体远程服务信息的JSON字符串按照URLEncode之后的字符串)
  • 根据拿到的服务提供信息解码得到服务提供者并返回

Server包实现

该包是对RPC服务注册与服务注入的实现包,内部包括了注册模块register和服务启动模块。

register模块

该模块提供了一个ServiceRegister的顶部接口,用于定义服务注册的规范,用户可以实现该接口,自定义注册规范与逻辑,danran-rpc具体的实现类为ZookeeperExportServiceRegister,提供Zookeeper注册中心实现。

ZookeeperExportServiceRegister

该类(后续简称ZESR)实现注册接口ServiceRegister,提供Zookeeper注册实现。

ZESR会根据用户提供的Zookeeper的地址,开启一个zkClient。

服务注册——register()

  • 获取服务提供者的host地址
  • 用户提供服务端口port,在port向外暴露服务
  • 封装服务包装类Service
  • 调用服务暴露exportService()

服务暴露——exportService()

  • 从Service中获取到服务名称(服务实现接口的全类名)
  • 服务地址拼接(和服务发现的逻辑一致)
  • 具体的服务(包含服务提供者的服务地址,以及服务实现接口)
1
2
3
4
5
6
7
> json复制代码{
> "address":"192.168.25.1:6666",
> "name":"common.api.BookService",
> "protocol":"danran"
> }
>
>
  • 向Zookeeper注册服务——创建节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码/***
* 服务暴露
*
* @param service 需要暴露的服务
*/
private void exportService(Service service) {
String serviceName = service.getName();
String uri = JSON.toJSONString(service);
try {
uri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 抽象的服务定位节点
String servicePath = "/rpc/" + serviceName + "/service";
logger.info("抽象服务[" + serviceName + "]注册地址 == " + servicePath);

if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath, true);
}
// 具体的服务实例地址
String uriPath = servicePath + "/" + uri;
logger.info("具体服务实例注册节点 == " + uriPath);

if (zkClient.exists(uriPath)) zkClient.delete(uriPath);// 如果存在,删除更新
zkClient.createEphemeral(uriPath);// 创建一个短暂的节点
}

RPC处理者——DefaultRpcProcessor

该类定义了RPC服务启动的具体流程,实现Spring的ApplicationListener,支持服务启动暴露,自动注入Service。

内部含有三个成员:

该类主要做两件事情:

  • 把用户用@RPCService标记的服务发布
  • 将用户用@InjectService标记的服务注入

这里使用了Spring提供的IOC容器,从容器中获取bean进行操作。

RPC服务启动
  1. 从Spring容器中取出所有用@RPCService标记的bean
  2. 遍历得到的bean列表,每一个bean是实际服务的提供类对象实例
  3. 获得bean的Class对象clazz,并通过clazz去获得他实现的接口(和客户端的接口是一致的,以此来关联起来客户端和服务端)
  4. 以父类(接口),实际的服务提供者包装,然后调用服务注册模块,注册到Zookeeper中
  5. 启动RPC服务实现
服务注入

将用户用@InjectService标记的服务注入。

同样使用Spring的容器,得到容器中的bean,然后判断每个bean是否有需要注入的服务,一旦发现有需要注入的属性,通过反射的方式,将属性注入为代理对象,一旦客户端发起服务调用,便会触发代理对象的invoke()方法,执行代理执行,得到需要的返回数据。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

新功能:Prometheus Agent 模式上手体验 拉模

发表于 2021-11-28

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」。

大家好,我是张晋涛。

Prometheus 几乎已经成为了云原生时代下监控选型的事实标准,它也是第二个从 CNCF 毕业的项目。

当前,Prometheus 几乎可以满足各种场景/服务的监控需求。我之前有写过一些文章介绍过 Prometheus 及其生态,本篇我们将聚焦于 Prometheus 最新版本中发布的 Agent 模式,对于与此主题无关的一些概念或者用法,我会粗略带过。

拉模式(Pull)和 推模式(Push)

众所周知,Prometheus 是一种拉模式(Pull)的监控系统,这不同于传统的基于推模式(Push)的监控系统。

什么是拉模式(Pull)呢?

Prometheus Pull model

待监控的服务自身或者通过一些 exporter 暴露出来一些 metrics 指标的接口,由 Prometheus 去主动的定时进行抓取/采集,这就是拉模式(Pull)。即由监控系统主动的去拉(Pull)目标的 metrics。

与之相对应的就是推模式(Push)了。

Monitor Push model

由应用程序主动将自身的一些 metrics 指标进行上报,监控系统再进行相对应的处理。如果对于某些应用程序的监控想要使用推模式(Push),比如:不易实现 metrics 接口等原因,可以考虑使用 Pushgateway 来完成。

对于拉模式(Pull)和推模式(Push)到底哪种更好的讨论一直都在继续,有兴趣的小伙伴可以自行搜索下。

这里主要是聚焦于单个 Prometheus 和应用服务之间交互的手方式。本篇我们从更上层的角度或者全局角度来看看当前 Prometheus 是如何做 HA、 持久化和集群的。

Prometheus HA/持久化/集群的方案

在大规模生产环境中使用时,很少有系统中仅有一个单实例 Prometheus 存在的情况出现。无论从高可用、数据持久化还是从为用户提供更易用的全局视图来考虑,运行多个 Prometheus 实例的情况就很常见了。

目前 Prometheus 主要有三种办法来将多个 Prometheus 实例的数据进行聚合,并给用户提供一个统一的全局视图。

  • 联邦(Federation):是最早期的 Prometheus 内置的数据聚合方案。这种方案下,可以使用某个中心 Prometheus 实例从叶子 Prometheus 实例上进行指标的抓取。这种方案下可以保留 metrics 原本的时间戳,整体也比较简单;
  • Prometheus Remote Read(远程读):可以支持从远程的存储中读取原始 metrics,注意:这里的远程存储可以有多种选择。当读取完数据后,便可对它们进行聚合,并展现给用户了;
  • Prometheus Remote Write(远程写):可以支持将 Prometheus 采集到 metrics 等写到远程存储中。用户在使用的时候,直接从远端存储中读取数据,并提供全局视图等;

Prometheus Agent 模式

Prometheus Agent 是自 Prometheus v2.32.0 开始将会提供的一项功能,它主要是采用上文中提到的 Prometheus Remote Write 的方式,将启用了 Agent 模式的 Prometheus 实例的数据写入到远程存储中。并借助远程存储来提供一个全局视图。

前置依赖

由于它使用了 Prometheus Remote Write 的方式,所以我们需要先准备一个 “远程存储” 用于 metrcis 的中心化存储。这里我们使用 Thanos 来提供此能力。当然,如果你想要使用其他的方案,比如: Cortex、influxDB 等也都是可以的。

准备远程存储

这里我们直接使用 Thanos 最新版本的容器镜像来进行部署。这里我们使用了 host 网络比较方便进行测试。

执行完这些命令后,Thanos receive 将会监听在 http://127.0.0.1:10908/api/v1/receive 用于接收”远程写入”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码➜  cd prometheus
➜ prometheus docker run -d --rm \
-v $(pwd)/receive-data:/receive/data \
--net=host \
--name receive \
quay.io/thanos/thanos:v0.23.1 \
receive \
--tsdb.path "/receive/data" \
--grpc-address 127.0.0.1:10907 \
--http-address 127.0.0.1:10909 \
--label "receive_replica=\"0\"" \
--label "receive_cluster=\"moelove\"" \
--remote-write.address 127.0.0.1:10908
59498d43291b705709b3f360d28af81d5a8daba11f5629bb11d6e07532feb8b6
➜ prometheus docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
59498d43291b quay.io/thanos/thanos:v0.23.1 "/bin/thanos receive…" 21 seconds ago Up 20 seconds receive

准备查询组件

接下来我们启动一个 Thanos 的 query 组件,跟 receive 组件连接,用于查询写入的数据。

1
2
3
4
5
6
7
8
9
10
11
bash复制代码➜  prometheus docker run -d --rm \
--net=host \
--name query \
quay.io/thanos/thanos:v0.23.1 \
query \
--http-address "0.0.0.0:39090" \
--store "127.0.0.1:10907"
10c2b1bf2375837dbda16d09cee43d95787243f6dcbee73f4159a21b12d36019
➜ prometheus docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c2b1bf2375 quay.io/thanos/thanos:v0.23.1 "/bin/thanos query -…" 4 seconds ago Up 3 seconds query

注意:这里我们配置了 --store 字段,指向了前面的 receive 组件。

打开浏览器访问 http://127.0.0.1:39090/stores ,如果一起顺利,你应该可以看到 receive 已经注册到了 store 中。

部署 Prometheus Agent 模式

这里我直接从 Prometheus 的 Release 页面 下载了它最新版本 v2.32.0 的二进制文件。解压后,你会发现目录中的内容和之前版本中是一致的。

这是因为 Prometheus Agent 模式现在是内置在 Prometheus 二进制文件中的,增加 --enable-feature=agent 选项即可启用。

准备配置文件

我们需要为它准备一份配置文件,注意, 需要配置 remote_write ,且不能有 alerting 之类的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码global:
scrape_interval: 15s
external_labels:
cluster: moelove
replica: 0

scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]

remote_write:
- url: 'http://127.0.0.1:10908/api/v1/receive'

配置文件另存为 prometheus.yml

启动

我们将它的日志级别设置成 debug 方便查看它的一些细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bash复制代码➜  ./prometheus --enable-feature=agent --log.level=debug --config.file="prometheus.yml" 
ts=2021-11-27T19:03:15.861Z caller=main.go:195 level=info msg="Experimental agent mode enabled."
ts=2021-11-27T19:03:15.861Z caller=main.go:515 level=info msg="Starting Prometheus" version="(version=2.32.0-beta.0, branch=HEAD, revision=c32725ba7873dbaa39c223410043430ffa5a26c0)"
ts=2021-11-27T19:03:15.861Z caller=main.go:520 level=info build_context="(go=go1.17.3, user=root@da630543d231, date=20211116-11:23:14)"
ts=2021-11-27T19:03:15.861Z caller=main.go:521 level=info host_details="(Linux 5.14.18-200.fc34.x86_64 #1 SMP Fri Nov 12 16:48:10 UTC 2021 x86_64 moelove (none))"
ts=2021-11-27T19:03:15.861Z caller=main.go:522 level=info fd_limits="(soft=1024, hard=524288)"
ts=2021-11-27T19:03:15.861Z caller=main.go:523 level=info vm_limits="(soft=unlimited, hard=unlimited)"
ts=2021-11-27T19:03:15.862Z caller=web.go:546 level=info component=web msg="Start listening for connections" address=0.0.0.0:9090
ts=2021-11-27T19:03:15.862Z caller=main.go:980 level=info msg="Starting WAL storage ..."
ts=2021-11-27T19:03:15.863Z caller=tls_config.go:195 level=info component=web msg="TLS is disabled." http2=false
ts=2021-11-27T19:03:15.864Z caller=db.go:306 level=info msg="replaying WAL, this may take a while" dir=data-agent/wal
ts=2021-11-27T19:03:15.864Z caller=db.go:357 level=info msg="WAL segment loaded" segment=0 maxSegment=0
ts=2021-11-27T19:03:15.864Z caller=main.go:1001 level=info fs_type=9123683e
ts=2021-11-27T19:03:15.864Z caller=main.go:1004 level=info msg="Agent WAL storage started"
ts=2021-11-27T19:03:15.864Z caller=main.go:1005 level=debug msg="Agent WAL storage options" WALSegmentSize=0B WALCompression=true StripeSize=0 TruncateFrequency=0s MinWALTime=0s MaxWALTime=0s
ts=2021-11-27T19:03:15.864Z caller=main.go:1129 level=info msg="Loading configuration file" filename=prometheus.yml
ts=2021-11-27T19:03:15.865Z caller=dedupe.go:112 component=remote level=info remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="Starting WAL watcher" queue=e6fa2a
ts=2021-11-27T19:03:15.865Z caller=dedupe.go:112 component=remote level=info remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="Starting scraped metadata watcher"
ts=2021-11-27T19:03:15.865Z caller=dedupe.go:112 component=remote level=info remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="Replaying WAL" queue=e6fa2a
ts=2021-11-27T19:03:15.865Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="Tailing WAL" lastCheckpoint= checkpointIndex=0 currentSegment=0 lastSegment=0
ts=2021-11-27T19:03:15.865Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="Processing segment" currentSegment=0
ts=2021-11-27T19:03:15.877Z caller=manager.go:196 level=debug component="discovery manager scrape" msg="Starting provider" provider=static/0 subs=[prometheus]
ts=2021-11-27T19:03:15.877Z caller=main.go:1166 level=info msg="Completed loading of configuration file" filename=prometheus.yml totalDuration=12.433099ms db_storage=361ns remote_storage=323.413µs web_handler=247ns query_engine=157ns scrape=11.609215ms scrape_sd=248.024µs notify=3.216µs notify_sd=6.338µs rules=914ns
ts=2021-11-27T19:03:15.877Z caller=main.go:897 level=info msg="Server is ready to receive web requests."
ts=2021-11-27T19:03:15.877Z caller=manager.go:214 level=debug component="discovery manager scrape" msg="Discoverer channel closed" provider=static/0
ts=2021-11-27T19:03:28.196Z caller=dedupe.go:112 component=remote level=info remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="Done replaying WAL" duration=12.331255772s
ts=2021-11-27T19:03:30.867Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="runShard timer ticked, sending buffered data" samples=230 exemplars=0 shard=0
ts=2021-11-27T19:03:35.865Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg=QueueManager.calculateDesiredShards dataInRate=23 dataOutRate=23 dataKeptRatio=1 dataPendingRate=0 dataPending=0 dataOutDuration=0.0003201718 timePerSample=1.3920513043478261e-05 desiredShards=0.0003201718 highestSent=1.638039808e+09 highestRecv=1.638039808e+09
ts=2021-11-27T19:03:35.865Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg=QueueManager.updateShardsLoop lowerBound=0.7 desiredShards=0.0003201718 upperBound=1.3
ts=2021-11-27T19:03:45.866Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg=QueueManager.calculateDesiredShards dataInRate=23.7 dataOutRate=18.4 dataKeptRatio=1 dataPendingRate=5.300000000000001 dataPending=355.5 dataOutDuration=0.00025613744 timePerSample=1.3920513043478263e-05 desiredShards=0.00037940358300000006 highestSent=1.638039808e+09 highestRecv=1.638039823e+09
ts=2021-11-27T19:03:45.866Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg=QueueManager.updateShardsLoop lowerBound=0.7 desiredShards=0.00037940358300000006 upperBound=1.3
ts=2021-11-27T19:03:45.871Z caller=dedupe.go:112 component=remote level=debug remote_name=e6fa2a url=http://127.0.0.1:10908/api/v1/receive msg="runShard timer ticked, sending buffered data" samples=265 exemplars=0 shard=0

从日志中可以看到,它会去向 http://127.0.0.1:10908/api/v1/receive 也就是我们一开始部署的 Thanos receive 发送数据。

查询数据

打开我们一开始部署好的 Thanos query, 输入任意 metrics 进行查询,可以查询到预期的结果。

但是如果我们直接访问开启了 Agent 模式的 Prometheus 的 UI 地址的时候,会直接报错,无法进行查询。这是由于 如果已开启 Agent 模式的 Prometheus 将会默认关闭其 UI 查询能力,报警以及本地存储等能力。

总结

本篇主要进行了 Prometheus Agent 的上手实践,通过 Thanos receive 接收来自 Prometheus Agent 的 metrics 上报,然后通过 Thanos query 进行结果的查询。

Prometheus Agent 并没有本质上改变 Prometheus 指标采集的方式,仍然还是继续使用拉模式(Pull)。

它的使用场景主要是进行 Prometheus 的 HA/数据持久化或集群。与现有的一些方案在架构上会略有重合,
但是有一些优势:

  • Agent 模式是 Prometheus 内置的功能;
  • 开启 Agent 模式的 Prometheus 实例,资源消耗更少,功能也会更单一,对于扩展一些边缘场景更有利;
  • 启用 Agent 模式后,Prometheus 实例几乎可以当作是一个无状态的应用,比较方便进行扩展使用;

过段时间就会发布正式版本了,你是否会尝试使用呢?


欢迎订阅我的文章公众号【MoeLove】

TheMoeLove

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring怎么又 bug 了,响应结果居然乱码了? 解析

发表于 2021-11-28

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」

换个方式访问该接口,示例如下:

期待”JavaEdge:dev 666”,但是运行上述代码后,你会发现结果却是下面这样:

why?

解析

这就要求精通 URL 的处理:

UriComponentsBuilder#toUriString:

URL Encode

调用栈如下:

至此,都还是正常的,但是当我们把 URL 转化成 String,再通过如下语句发送请求时:

会发现,它会再进行一次编码:

至此,你应该理解为啥出问题了:依案例代码会执行 2 次编码(Encode),所以最终获取意外惊喜!

2 次编码后:

修正

避免多次转化而发生多次编码:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hadoop 入门教程 Hadoop 教程

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

Hadoop 教程

  1. 前期准备

  • IDEA安装
    • IDEA2020.3.3
  • JDK安装
    • jdk18绿色版
  • IDEA中JDK配置
  • VMware安装
  • Hadoop 虚拟机
    • 待补充
  1. HDFS启动

1
2
shell复制代码cd app/hadoop-2.6.0-cdh5.7.0/sbin/
./start-dfs.sh

在这里插入图片描述

  1. Hadoop启动失败解决方法

  • 重新编辑本机的hosts文件
1
bash复制代码sudo vim /etc/hosts
  • 将 hadoop000 与 localhost 均改为本机ip
    在这里插入图片描述
    在这里插入图片描述
  1. Hadoop Shell命令

  • 浏览器可视化文件系统
    在这里插入图片描述
  • 路径遍历
    • hadoop fs -ls [路径]
  • 查看文件
    • hadoop fs -cat [文件路径]
    • eg:hadoop fs -cat /hadoopruochen/test/ruochen.txt
  • 新建文件夹
    • hadoop fs -mkdir -p [路径]
    • -p:递归新建
    • eg:hadoop fs -mkdir -p /hadoopruochen/test
  • 传文件到 Hadoop
    • hadoop fs -put [文件路径] [hadoop路径]
    • eg:hadoop fs -put ruochen.txt /hadoopruochen/test
  • 下载 Hadoop 文件到本地
    • hadoop fs -get [hadoop文件路径] [本地路径]
    • eg:hadoop fs -get /hadoopruochen/test/ruochen.txt haha.txt
  • 移动文件
    • hadoop fs -mv [源路径] [目的路径]
    • eg:hadoop fs -mv /hadoopruochen/test/ruochen.txt /user
  • 删除文件
    • hadoop fs -rm [-r] [文件]
    • eg:hadoop fs -rm /hadoopruochen
    • eg:hadoop fs -rm -r /hadoopruochen
  1. Java 操作 HDFS API

5.1. 新建项目

  • 新建一个空项目,我这里起名为BigData
    在这里插入图片描述
  • 新建一个module
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • Finish 即可
  • pom.xml如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
xml复制代码<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.neusoft</groupId>
<artifactId>hadoopdemo</artifactId>
<version>1.0-SNAPSHOT</version>

<name>hadoopdemo</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<hadoop.version>2.6.0-cdh5.7.0</hadoop.version>
</properties>
<repositories>
<repository>
<id>cloudera</id>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
</repositories>

<dependencies>
<!-- 添加hadoop依赖-->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

5.2. 测试

5.2.1 新建文件夹

  • 接下来,我们使用 Java 连接 hdfs,并新建一个文件夹
  • 在test下新建HDFSApp.java,如下
    在这里插入图片描述
  • 通过测试方法连接HDFS,并新建一个/ruochen/test2文件夹,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码package com.neusoft.hdfs;

import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.apache.hadoop.conf.Configuration;

import java.net.URI;

public class HDFSApp {
Configuration configuration = null;
FileSystem fileSystem = null;
public static final String HDFS_PATH = "hdfs://192.168.10.128:8020";

@Test
public void mkdir() throws Exception {
fileSystem.mkdirs(new Path("/ruochen/test2"));
}

// Java 连接hdfs 需要先建立一个连接
// 测试方法执行之前要执行的操作
@Before
public void setUp() throws Exception {
System.out.println("开始建立与HDFS的连接");
configuration = new Configuration();
fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, "hadoop");
}

// 测试之后要执行的代码
@After
public void tearDown() {
configuration = null;
fileSystem = null;
System.out.println("关闭与HDFS的连接");
}
}
  • 然后运行mkdir()函数,运行完后我们可以看到已经新建了一个文件夹
    在这里插入图片描述

5.2.2 新建文件

  • 新建文件代码如下
1
2
3
4
5
6
7
8
9
java复制代码    // 创建文件
@Test
public void create() throws Exception {
Path path = new Path("/ruochen/test1/hello.txt");
FSDataOutputStream outputStream = fileSystem.create(path);
outputStream.write("hello world".getBytes());
outputStream.flush();
outputStream.close();
}
  • 运行结束后,我们通过shell脚本查看一下
    在这里插入图片描述

5.2.3 修改文件名称

  • Java代码如下
1
2
3
4
5
6
7
java复制代码   // rename文件
@Test
public void rename() throws Exception {
Path oldPath = new Path("/ruochen/test1/hello.txt");
Path newPath = new Path("/ruochen/test1/xixi.txt");
fileSystem.rename(oldPath, newPath);
}
  • 运行结果如下
    在这里插入图片描述

5.2.4 查看文件

  • Java代码如下
1
2
3
4
5
6
7
8
java复制代码    // 查看文件
@Test
public void cat() throws Exception {
Path path = new Path("/ruochen/test1/xixi.txt");
FSDataInputStream inputStream = fileSystem.open(path);
IOUtils.copyBytes(inputStream, System.out, 1024);
inputStream.close();
}
  • 运行结果
    在这里插入图片描述

5.2.5 上传文件

  • Java 代码如下
1
2
3
4
5
6
7
java复制代码    // 上传文件
@Test
public void upload() throws Exception {
Path localPath = new Path("cifar-10-python.tar.gz");
Path hdfsPath = new Path("/");
fileSystem.copyFromLocalFile(localPath, hdfsPath);
}
  • 运行完成后,我们可以看到 hdfs 已经成功显示刚才上传的文件
    在这里插入图片描述

5.2.6 下载文件

  • Java 代码
1
2
3
4
5
6
7
java复制代码    // 下载文件
@Test
public void download() throws Exception {
Path hdfsPath = new Path("/hadoop-2.6.0-cdh5.7.0.tar.gz");
Path localPath = new Path("./down/hadoop-2.6.0-cdh5.7.0.tar.gz");
fileSystem.copyToLocalFile(false, hdfsPath, localPath, true);
}
  • 运行完后我们可以看到当前目录 down 下已经有了刚刚下载的文件
    在这里插入图片描述
    在这里插入图片描述
  1. Java 实现 WordCount

这里要注意在 main 下操作,test下是用来测试的

  • 新建一个 WordCountApp 类
    在这里插入图片描述
  • Java 代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
java复制代码package com.neusoft;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
* 词频统计
*/
public class WordCountApp {
/**
* map 阶段
*/
public static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
LongWritable one = new LongWritable(1);

@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 分
String line = value.toString();
// 拆分
String[] s = line.split(" ");
for (String word : s) {
// 输出
context.write(new Text(word), one);
}
}
}

/**
* reduce 阶段
*/
public static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long sum = 0;
// 合并统计
for (LongWritable value : values) {
// 求和
sum += value.get();
}
context.write(key, new LongWritable(sum));
}
}

public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration, "wordcount");
job.setJarByClass(WordCountApp.class);

// 设置 map 相关参数
FileInputFormat.setInputPaths(job, new Path(args[0]));
job.setMapperClass(MyMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);

// 设置 reduce 相关参数
job.setReducerClass(MyReducer.class);
job.setOutputKeyClass(MyReducer.class);
job.setOutputValueClass(LongWritable.class);

Path outPath = new Path(args[1]);
FileSystem fileSystem = FileSystem.get(configuration);
if (fileSystem.exists(outPath)) {
// 删除文件
fileSystem.delete(outPath, true);
System.out.println("输出路径已存在, 已被删除");
}
FileOutputFormat.setOutputPath(job, outPath);

// 控制台输出详细信息
// 输出:1 不输出:0
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
  • 打包程序
    在这里插入图片描述
    在这里插入图片描述
  • 打包完成后,将 jar 包上传到 hadoop 虚拟机
    在这里插入图片描述
    在这里插入图片描述
  • 首先通过shell命令将输出文件夹删除,不然重复执行会报错
1
shell复制代码hadoop fs -rm -r /output/wc
  • 然后执行下列操作
1
shell复制代码hadoop jar hadoopdemo-1.0-SNAPSHOT.jar com.neusoft.WordCountApp hdfs://hadoop000:8020/ruochenchen.txt hdfs://hadoop000:8020/output/wc

hadoop jar hadoopdemo-1.0-SNAPSHOT.jar com.neusoft.WordCountApp 输入文件 输出文件

在这里插入图片描述

  • 然后我们可以看到作业中有显示
    在这里插入图片描述
  • 通过 cat 命令可以查看一下输出的文件
    在这里插入图片描述
1
shell复制代码hadoop fs -cat /output/wc/part-r-00000

在这里插入图片描述

最后,欢迎大家关注我的个人微信公众号 『小小猿若尘』,获取更多IT技术、干货知识、热点资讯

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Arthas-初识常用命令 前言 1dashboard 仪

发表于 2021-11-28

前言

如何下载、安装、启动 Arthas 可点击此处Arthas在Docker容器中的使用-环境搭建 或 Arthas 官网

1.dashboard 仪表板

  • ctrl+c 中断执行,退出仪表板
  • cls 清理当前页面,类似 clear
  • Tab 键,可自动补全 arthas 命令

image.png

2.thread 查看线程

  • thread 查看当前进程的所有线程image.png
  • thread ID 查看指定线程ID的线程image.png
  • thread -n 3 展示当前最忙的前3个线程并打印堆栈信息image.png
  • thread -b 找出当前阻塞其它线程的线程,排查死锁image.png
  • thread -i 1000 -n 3 指定采样时间间隔,并展示最忙碌的3个线程image.png
  • thread --state 线程状态 查看处于锁定状态的线程image.png

3.jad 包名.类名 反编译 (需要类被加载到JVM中)

  • jad 包名.类名 包含有类加载器+位置+源码 image.png
  • jad 包名.类名 --source-only 只包含反编译后的源码image.png
  • jad 包名.类名 方法名 反编译指定的方法image.png
  • jad 包名.类名 > 存放路径名 反编译类,并存放在指定的文件下image.png

4.watch 观察指定方法的调用情况

  • watch 包名.类名 方法名 "{params,returnObj}" -x 2,监视方法出参和返回值,-x表示指定输出结果的属性遍历深度,默认为1,params表示所有参数数组,returnObject表示返回值image.png
  • watch 包名.类名 方法名 "{params,returnObj}" -x 2 -b 观察方法入参,-b表示在方法调用之前观察,此时没有返回值-e表示在方法异常之后观察,-s表示在方法返回之后观察,-f表示方法结束之后(正常返回和异常返回)观察image.png
  • watch 包名.类名 方法名 "target" -x 2观察当前对象中的所有属性,target表示当前对象image.png
  • watch 包名.类名 方法名 "target.field_name" -x 2 观察当前对象中的指定属性 target.field_name指定对象中的某个属性image.png
  • watch 包名.类名 方法名 "{params,target,returnObj}" -x 2 -b -s -n 2 同时观察方法调用前和方法返回后,参数-n 2表示只执行两次。params表示参数,target表示执行方法的对象,returnObj表示返回值image.png

5.退出 arthas

  • quit 或 exit 只是退出当前连接,端口会保持开放
  • stop 结束会话,完全退出 arthas

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【golang】 语言基础 Go 基础 流程和函数 stru

发表于 2021-11-28

Go 基础

内置基础类型

数值类型

rune,int8,int16,int32,int64 和 byte,uint8,uint16,uint32,uint64,其中 rune 是 int32 的别称,byte 是 uint8 的别称。

浮点数 的类型有 float32 和 float64 两种(没有 float 类型)。

复数 的类型有 complex128(64 位实数 + 64 位虚数)和 complex64(32 位实数 + 32 位虚数)。复数的形式为 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,而最后的 i 是虚数单位。

1
2
3
go复制代码var c complex64 = 5 + 5i
// output: (5+5i)
fmt.Printf("Value is: %v", c)

string

在 Go 中字符串是不可变的,但如果真的想要修改怎么办呢?

1
2
3
4
5
go复制代码s := "hello"
c := []byte(s) // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)

当需要对一个字符串进行频繁的操作时,谨记在 go 语言中字符串是不可变的(类似 java 和 c#)。使用诸如 a += b 形式连接字符串效率低下,尤其在一个循环内部使用这种形式。这会导致大量的内存开销和拷贝。应该使用一个字符数组代替字符串,将字符串内容写入一个缓存中。

1
2
3
4
5
6
go复制代码var b bytes.Buffer
...
for condition {
b.WriteString(str) // 将字符串 str 写入缓存 buffer
}
return b.String()

注意:由于编译优化和依赖于使用缓存操作的字符串大小,当循环次数大于 15 时,效率才会更佳。

数组

数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。

1
2
3
4
go复制代码var variable_name [SIZE] variable_type
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [5]float32{1:2.0,3:7.0}

多维数组

1
go复制代码var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码func main() {
// Step 1:创建数组
values := [][]int{}

// Step 2:使用 appped() 函数向空的二维数组添加两行一维数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1)
values = append(values, row2)

// Step 3:显示两行数据
fmt.Println("Row 1")
fmt.Println(values[0])
fmt.Println("Row 2")
fmt.Println(values[1])

// Step 4:访问第一个元素
fmt.Println("第一个元素为:")
fmt.Println(values[0][0])
}

slice

slice 是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值。

slice 是一个结构体,这个结构体包含了三个元素:

  • 引用数组指针地址;
  • 切片的目前使用长度;
  • 切片的容量;
1
2
3
4
go复制代码// 默认是 nil
var identifier []type
s := make([]int, len, cap)
y := s[low:high:max]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func main() {
s := []int{1, 2, 3}
a := s[0:1]
s[0] = 888
fmt.Printf("a: %p\n", a)
fmt.Println("a", a, len(a), cap(a))
fmt.Printf("s: %p\n", s)
fmt.Println("s", s, len(s), cap(s))
s = append(s, 4)
fmt.Println("扩容后")
s[0] = 666
fmt.Printf("a: %p\n", a)
fmt.Println("a", a, len(a), cap(a))
fmt.Printf("s: %p\n", s)
fmt.Println("s", s, len(s), cap(s))
}
1
2
3
4
5
6
7
8
9
ini复制代码a: 0xc00011a000
a [888] 1 3
s: 0xc00011a000
s [888 2 3] 3 3
扩容后
a: 0xc00011a000
a [888] 1 3
s: 0xc000120000
s [666 2 3 4] 4 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码func main() {
var (
arr = [3]int{1, 2, 3}
slice = []int{1, 2, 3}
)

changeArr(&arr)
changeSlice(slice)

fmt.Println("arr", arr)
fmt.Println("slice", slice)
}

func changeArr(arr *[3]int) {
arr[0] = 100
}

func changeSlice(slice []int) {
slice[0] = 100
}
1
2
css复制代码arr [100 2 3]
slice [100 2 3]

slice 和垃圾回收

切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性有时会导致程序占用多余的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码var digitRegexp = regexp.MustCompile("[0-9]+")

// 返回的 []byte 指向的底层是整个文件的数据,只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}

// 可以通过拷贝需要的部分到一个新的切片中
func FindFileDigits(filename string) []byte {
fileBytes, _ := ioutil.ReadFile(filename)
b := digitRegexp.FindAll(fileBytes, len(fileBytes))
c := make([]byte, 0)
for _, bytes := range b {
c = append(c, bytes...)
}
return c
}

map

1
2
3
go复制代码// 默认是 nil,nil map 不能用来存放键值对
var map_variable map[key_data_type]value_data_type
map_variable := make(map[key_data_type]value_data_type)

make & new

make 只能创建 slice、map 和 channel,并且返回一个有初始值(非零)的 T 类型,而不是 *T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个 slice,是一个包含指向数据(内部 array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice 为 nil。对于 slice、map 和 channel来说,make 初始化了内部的数据结构,填充适当的值。

内建函数 new 本质上说跟其它语言中的同名函数功能一样,new(T) 分配了零值填充 T 类型的内存空间,并且返回其地址,即一个 *T 类型的值。用 Go 的术语说,它返回了一个指针,指向新分配的类型 T 的零值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码// OK
y := new(Bar)
(*y).thingOne = "hello"
(*y).thingTwo = 1

// NOT OK
z := make(Bar) // 编译错误:cannot make type Bar
(*z).thingOne = "hello"
(*z).thingTwo = 1

// OK
x := make(Foo)
x["x"] = "goodbye"
x["y"] = "world"

// NOT OK
u := new(Foo)
(*u)["x"] = "goodbye" // 运行时错误!! panic: assignment to entry in nil map
(*u)["y"] = "world"

使用 new 和 make 创建 map 时的差异

使用 new 来创建 map 时,返回的内容是一个指针,这个指针指向了一个所有字段全为 0 的值 map 对象,需要初始化后才能使用,而使用 make 来创建 map 时,返回的内容是一个引用,可以直接使用。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// 使用 new 创建一个 map 指针
ma := new(map[string]int)
// 第一种初始化方法
*ma = map[string]int{}
(*ma)["a"] = 44
fmt.Println(*ma)

// 第二种初始化方法
*ma = make(map[string]int, 0)
(*ma)["b"] = 55
fmt.Println(*ma)

// 第三种初始化方法
mb := make(map[string]int, 0)
mb["c"] = 66
*ma = mb
(*ma)["d"] = 77
fmt.Println(*ma)
1
2
3
4
go复制代码// 使用 make 来创建并使用 map
ma := make(map[string]int)
ma["a"] = 33
fmt.Println(ma)

结论:

  • 切片、map 和通道,使用 make。
  • 数组、结构体和所有的值类型,使用 new。

iota

常量中的数据类型只可以是布尔型、数值型(整数型、浮点型和复数)和字符串型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)

const (
a = iota // 0
b // 1
c // 2
d = "ha" // 独立值,iota += 1
e // "ha",iota += 1
f = 100 // iota += 1
g // 100,iota +=1
h = iota // 7,恢复计数
i // 8
)

const (
h, i, j = iota, iota, iota // h = 0、i = 0、j = 0,因为 iota 在同一行
)

error

1
2
3
4
go复制代码err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码type User struct {
username string
password string
}

func (p *User) init(username string, password string) (*User, string) {
if "" == username || "" == password {
return p, p.Error()
}
p.username = username
p.password = password
return p, ""
}

func (p *User) Error() string {
return "Usernam or password shouldn't be empty!"
}

func main() {
var user User
user1, _ := user.init("", "")
fmt.Println(user1)
}
1
rust复制代码Usernam or password shouldn't be empty!

流程和函数

流程控制

switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码func main() {

switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
}
1
2
3
arduino复制代码2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true

for

1
2
3
4
go复制代码sum := 1
for sum < 1000 {
sum += sum
}

break / continue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码func main() {

// 不使用标记
fmt.Println("---- break ----")
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
break
}
}

// 使用标记
fmt.Println("---- break label ----")
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
break / continue re
}
}
}

goto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func main() {
/* 定义局部变量 */
var a int = 10

/* 循环 */
LOOP:
for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}
1
2
3
4
5
6
7
8
9
less复制代码a的值为 : 10
a的值为 : 11
a的值为 : 12
a的值为 : 13
a的值为 : 14
a的值为 : 16
a的值为 : 17
a的值为 : 18
a的值为 : 19

函数

变参

1
2
go复制代码// 变量 arg 是一个 int 的 slice
func myfunc(arg ...int) {}

函数作为实参

在 Go 中函数也是一种变量,可以通过 type 来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。

1
2
3
4
5
6
7
8
9
go复制代码func main() {
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}

/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
1
复制代码2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
go复制代码type testInt func(int) bool // 声明了一个函数类型

func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}

func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}

// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}

func main() {
slice := []int{1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println("Even elements of slice are: ", even)
}
1
2
3
ini复制代码slice = [1 2 3 4 5 7]
Odd elements of slice are: [1 3 5 7]
Even elements of slice are: [2 4]

闭包

Go 语言支持匿名函数,可作为闭包。匿名函数是一个“内联”语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}

func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()

/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
1
2
3
4
5
复制代码1
2
3
1
2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func add(x1, x2 int) func(int, int) (int, int, int) {
i := 0
return func(x3, x4 int) (int, int, int) {
i += 1
return i, x1 + x2, x3 + x4
}
}

func main() {
add_func := add(1, 2)
fmt.Println(add_func(1, 1))
fmt.Println(add_func(0, 0))
fmt.Println(add_func(2, 2))
}
1
2
3
复制代码1 3 2
2 3 0
3 3 4

工厂函数案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func main() {
addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")

fmt.Println(addBmp("file"))
fmt.Println(addJpeg("file"))
}

func MakeAddSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
1
2
复制代码file.bmp
file.jpeg

defer

当函数执行到最后时,defer语句会按照逆序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码func trace(s string) string {
fmt.Println("entering:", s)
return s
}

func un(s string) {
fmt.Println("leaving:", s)
}

func a() {
defer un(trace("a"))
fmt.Println("in a")
}

func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}

func main() {
b()
}
1
2
3
4
5
6
makefile复制代码entering: b
in b
entering: a
in a
leaving: a
leaving: b

使用 defer 语句来记录函数的参数与返回值。

1
2
3
4
5
6
7
8
9
10
go复制代码func func1(s string) (n int, err error) {
defer func() {
log.Printf("func1(%q) = %d, %v", s, n, err)
}()
return 7, io.EOF
}

func main() {
func1("Go")
}
1
css复制代码2022/02/28 22:36:18 func1("Go") = 7, EOF

循环内的 defer 没有执行,所以文件一直没有关闭。垃圾回收机制可能会自动关闭文件,但是这会产生一个错误。

1
2
3
4
5
6
7
8
9
go复制代码for _, file := range files {
if f, err = os.Open(file); err != nil {
return
}
// 这是错误的方式,当循环结束时文件没有关闭
defer f.Close()
// 对文件进行操作
f.Process(data)
}

defer 仅在函数返回时才会执行,在循环的结尾或其他一些有限范围的代码内不会执行。

1
2
3
4
5
6
7
8
9
go复制代码for _, file := range files {
if f, err = os.Open(file); err != nil {
return
}
// 对文件进行操作
f.Process(data)
// 关闭文件
f.Close()
}

panic & recover

panic 是一个内建函数,可以中断原有的控制流程,进入一个 panic 状态中。当函数 F 调用 panic,函数 F 的执行被中断,但是 F 中的延迟函数会正常执行,然后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了 panic。这一过程继续向上,直到发生 panic 的 goroutine 中所有调用的函数返回,此时程序退出。可以直接调用 panic 产生,也可以由运行时错误产生,例如访问越界的数组。

recover 是一个内建函数,可以让进入 panic 状态的 goroutine 恢复过来。recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil,并且没有其它任何效果。如果当前的 goroutine 陷入 panic 状态,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

1
2
3
4
5
6
7
8
9
go复制代码func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() // 执行函数 f,如果 f 中出现了 panic,那么就可以恢复回来
return
}

main 函数 & init 函数

Go 里面有两个保留的函数:init 函数(能够应用于所有的 package)和 main 函数(只能应用于 main package),这两个函数在定义时不能有任何的参数和返回值。虽然一个 package 里面可以写任意多个 init 函数,但强烈建议在一个 package 中每个文件只写一个 init 函数。

Go 程序会自动调用 init() 和 main(),所以不需要在任何地方调用这两个函数。

程序的初始化和执行都起始于 main 包,如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。

image.png

import

Go 程序是通过 package 来组织的。

  • 包名与文件名没有直接关系;
  • 包名与文件夹名没有直接关系;
  • 同一个文件夹下的文件只能有一个包名,否则编译报错;
  • 只有包名为 main 的源码文件可以包含 main 函数;
  • 一个可执行程序有且仅有一个 main 包;
1
2
3
4
5
6
7
8
9
10
11
12
go复制代码// 点操作,调用的时候只需要 Println(),而不需要 fmt.Println()
// fmt 是 Go 语言的标准库,其实是去 GOROOT 环境变量指定目录下去加载该模块
import . "fmt"

// _ 操作,引入该包,而不直接使用包里面的函数,而是调用了该包里面的 init 函数
import _ "github.com/ziutek/mymysql/godrv"

// 相对路径,当前文件同一目录的 model 目录,但是不建议这种方式来 import
import "./model"

// 绝对路径,加载 gopath/src/shorturl/model 模块
import "shorturl/model"

struct

方法

Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
go复制代码type Foo struct {
name string
}

func (f *Foo) PointerMethod() {
fmt.Println("pointer method on", f.name)
}

func (f Foo) ValueMethod() {
fmt.Println("value method on", f.name)
}

func NewFoo() Foo { // 返回一个右值
return Foo{name: "right value struct"}
}

func main() {
f1 := Foo{name: "value struct"}
f1.PointerMethod() // 编译器会自动插入取地址符,变为 (&f1).PointerMethod()
f1.ValueMethod()

f2 := &Foo{name: "pointer struct"}
f2.PointerMethod()
f2.ValueMethod() // 编译器会自动解引用,变为 (*f2).PointerMethod()

NewFoo().ValueMethod()
NewFoo().PointerMethod() // Error!!!
}
1
2
3
csharp复制代码# command-line-arguments
.\main.go:33:10: cannot call pointer method on NewFoo()
.\main.go:33:10: cannot take the address of NewFoo()

看来编译器首先试着给 NewFoo() 返回的右值调用 pointer method,出错;然后试图给其插入取地址符,未果,就只能报错了。

可以被寻址的是左值,既可以出现在赋值号左边也可以出现在右边;不可以被寻址的即为右值,比如函数返回值、字面值、常量值等等,只能出现在赋值号右边。

指针或值作为接收者

在值和指针上调用方法:可以有连接到类型的方法,也可以有连接到类型指针的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码type B struct {
thing int
}

func (b *B) change() { b.thing = 1 }

func (b B) write() string { return fmt.Sprint(b) }

func main() {
var b1 B // b1 是值
b1.change()
fmt.Println(b1.write())

b2 := new(B) // b2 是指针
b2.change()
fmt.Println(b2.write())
}
1
2
复制代码{1}
{1}

将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个指针,而不是一个值类型,go 编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,并不一定有太大的收获。

匿名字段、继承、重写

在 Go 中,类型就是类(数据和关联的方法)。继承有两个好处:代码复用和多态。在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫组件编程(Component Programming)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
go复制代码type Skills []string

type Human struct {
name string
age int
weight int
}

func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s\n", h.name)
}

type Student struct {
Human // 匿名字段,struct
Skills // 匿名字段,自定义的类型 string slice
int // 内置类型作为匿名字段
speciality string
age int
}

func (s *Student) SayHi() {
fmt.Printf("Hi, I Student am %s\n", s.name)
}

func main() {
jane := Student{Human: Human{"Jane", 35, 100}, speciality: "Biology"}

jane.Human.name = "Jane1"
jane.Human.age = 23
jane.age = 22
fmt.Println("Her name is ", jane.Human.name)
fmt.Println("Her Human.age is ", jane.Human.age)
fmt.Println("Her age is ", jane.age)
fmt.Println("Her speciality is ", jane.speciality)

jane.Skills = []string{"anatomy"}
fmt.Println("Her skills are ", jane.Skills)
jane.Skills = append(jane.Skills, "physics", "golang")
fmt.Println("Her skills now are ", jane.Skills)

jane.int = 3
fmt.Println("Her preferred number is", jane.int)

jane.Human.SayHi()
jane.SayHi()
}
1
2
3
4
5
6
7
8
csharp复制代码Her name is  Jane1
Her Human.age is 23
Her age is 22
Her speciality is Biology
Her skills are [anatomy]
Her skills now are [anatomy physics golang]
Her preferred number is 3
Hi, I am Jane1

interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
go复制代码type Phone interface {
call() string
}

type Android struct {
brand string
}

type IPhone struct {
version string
}

func (android Android) call() string {
return "I am Android " + android.brand
}

func (iPhone IPhone) call() string {
return "I am iPhone " + iPhone.version
}

func printCall(p Phone) {
fmt.Println(p.call() + ", I can call you!")
}

func main() {
var vivo = Android{brand: "Vivo"}
var hw = Android{"HuaWei"}

i7 := IPhone{"7 Plus"}
ix := IPhone{"X"}

printCall(vivo)
printCall(hw)
printCall(i7)
printCall(ix)
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…137138139…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%