通过示例学习技术-2023-二-
通过示例学习技术 2023(二)
设计一个内存缓存
目录
-
概述
-
设置(key int, value int)")
-
获取(key int)")
-
低级设计
-
UML 图
-
程序
-
结论
概述
目标是设计一个内存缓存。以下是要求
-
它应该支持设置和获取操作
-
设置和获取操作的时间复杂度为 O(1)
-
假设缓存的最大容量为 3。当缓存已满并且有一个新键需要插入时,必须删除缓存中的一个现有条目
-
删除应该基于驱逐算法——FIFO 或 LRU
-
假设缓存中的键和值都是字符串类型
-
它应该支持的驱逐算法是先进先出(FIFO)和最近最少使用(LRU)
-
驱逐算法应该是可插拔的。这意味着你应该能够在运行时更改驱逐算法。
以下是我们设计中的高层内容
-
我们将拥有一个缓存类,作为与客户端交互的接口
-
缓存类将使用映射和双向链表的组合来存储所有内容。使用映射和双向链表是为了确保即使在驱逐情况下,获取和设置操作的时间复杂度依然是 O(1)。我们将在本教程的后续部分详细介绍如何实现。
-
映射将具有字符串类型的键,以及指向双向链表节点的指针类型值
-
每个双向链表的节点将包含键和值。每个节点还将有一个指向双向链表中前一个节点的指针,以及一个指向下一个节点的指针
-
将会有一个驱逐算法接口。将有LRU和FIFO类来实现这个驱逐算法接口
-
缓存类还将嵌入一个驱逐算法接口实例。
驱逐算法接口的存在是为了将我们的缓存类与算法解耦,以便我们可以在运行时更改算法。而且,当添加新算法时,缓存类不应发生变化。这就是策略设计模式的应用场景。策略模式建议创建一个算法家族,每个算法都有自己的类。这些类遵循相同的接口,使得算法在这个家族中是可互换的。假设通用接口名称是驱逐算法,那么FIFO和LRU类将实现这个接口。
现在我们的主 Cache 类将嵌入 evictionAlgo 接口。我们的 Cache 类不会在自己内部实现所有类型的驱逐算法,而是将所有的工作委托给 evictionAlgo 接口。由于 evictionAlgo 是一个接口,我们可以在运行时改变算法,使其成为 LRU 或 FIFO,而无需修改 Cache 类。
此外,我们将使用工厂设计模式来创建每个算法的实例,即 FIFO、LRU。
让我们看看 Get 和 Set 如何在 O(1) 时间内工作。
Set(key int, value int)
对于任何 set 操作,它将首先创建一个带有提供的 key 和 value 的双向链表节点。然后,在 map 中创建一个条目,key 为输入的 key,value 为该节点的地址。节点创建后,有两种情况:
-
缓存未满 – 在这种情况下,它将控制权交给当前的 evictionAlgorithm 接口。evictionAlgorithm 接口将根据当前的驱逐算法,将该节点插入到双向链表的末尾或前面。这里的每个操作都是 O(1)。
-
缓存已满 – 在这种情况下,它将控制权交给当前的 evictionAlgorithm 接口,根据当前的驱逐算法驱逐其中一个节点。驱逐后,它会插入新节点。这里的每个操作都是 O(1)。
Get(key int)
对于任何 Get 操作,它将首先检查 map 中是否存在给定的 key。如果存在,它将获取该 key 在 map 中指向的节点的地址。然后,它会从节点中获取值。接着,它会将控制权交给当前的 evictionAlgorithm 接口。evictionAlgorithm 接口将根据当前的驱逐算法,重新排列当前节点在双向链表中的位置,可能是末尾或前面。这里的每个操作都是 O(1)。
低级设计
以下是用 Go 编程语言表达的低级设计。稍后我们将看到一个 UML 图以及一个工作示例。
Cache 类
type cache struct {
dll *doublyLinkedList
storage map[string]string
evictionAlgo evictionAlgo
capacity int
maxCapacity int
}
func (c *cache) setEvictionAlgo(e evictionAlgo)
func (c *cache) add(key, value string)
func (c *cache) get(key string)
func (c *cache) evict()
双向链表
type node struct {
data string
prev *node
next *node
}
type doublyLinkedList struct {
len int
tail *node
head *node
}
func initDoublyList() *doublyLinkedList {
return &doublyLinkedList{}
}
func (d *doublyLinkedList) AddFrontNodeDLL(data string)
func (d *doublyLinkedList) AddEndNodeDLL(data string)
func (d *doublyLinkedList) MoveToFront(node *node) error
func (d *doublyLinkedList) MoveToBack(node *node) error
func (d *doublyLinkedList) Size() int
驱逐算法接口
type evictionAlgo interface {
evict(c *cache)
}
func createEvictioAlgo(algoType string) evictionAlgo
FIFO 算法 – 它实现了驱逐算法接口。
type fifo struct {
}
func (l *fifo) evict(c *cache)
LRU 算法 – 它实现了驱逐算法接口
type lru struct {
}
func (l *lru) evict(c *cache)
UML 图

程序
这是用 Go 编程语言编写的完整工作代码,如果有兴趣的人可以查看。
doublylinklist.go
package main
import "fmt"
type node struct {
key string
value string
prev *node
next *node
}
type doublyLinkedList struct {
len int
tail *node
head *node
}
func initDoublyList() *doublyLinkedList {
return &doublyLinkedList{}
}
func (d *doublyLinkedList) AddToFront(key, value string) {
newNode := &node{
key: key,
value: value,
}
if d.head == nil {
d.head = newNode
d.tail = newNode
} else {
newNode.next = d.head
d.head.prev = newNode
d.head = newNode
}
d.len++
return
}
func (d *doublyLinkedList) RemoveFromFront() {
if d.head == nil {
return
} else if d.head == d.tail {
d.head = nil
d.tail = nil
} else {
d.head = d.head.next
}
d.len--
}
func (d *doublyLinkedList) AddToEnd(node *node) {
newNode := node
if d.head == nil {
d.head = newNode
d.tail = newNode
} else {
currentNode := d.head
for currentNode.next != nil {
currentNode = currentNode.next
}
newNode.prev = currentNode
currentNode.next = newNode
d.tail = newNode
}
d.len++
}
func (d *doublyLinkedList) Front() *node {
return d.head
}
func (d *doublyLinkedList) MoveNodeToEnd(node *node) {
prev := node.prev
next := node.next
if prev != nil {
prev.next = next
}
if next != nil {
next.prev = prev
}
if d.tail == node {
d.tail = prev
}
if d.head == node {
d.head = next
}
node.next = nil
node.prev = nil
d.len--
d.AddToEnd(node)
}
func (d *doublyLinkedList) TraverseForward() error {
if d.head == nil {
return fmt.Errorf("TraverseError: List is empty")
}
temp := d.head
for temp != nil {
fmt.Printf("key = %v, value = %v, prev = %v, next = %v\n", temp.key, temp.value, temp.prev, temp.next)
temp = temp.next
}
fmt.Println()
return nil
}
func (d *doublyLinkedList) Size() int {
return d.len
}
evictionAlgorithm.go
package main
type evictionAlgo interface {
evict(c *Cache) string
get(node *node, c *Cache)
set(node *node, c *Cache)
set_overwrite(node *node, value string, c *Cache)
}
func createEvictioAlgo(algoType string) evictionAlgo {
if algoType == "fifo" {
return &fifo{}
} else if algoType == "lru" {
return &lru{}
}
return nil
}
lru.go
package main
import "fmt"
type lru struct {
}
func (l *lru) evict(c *Cache) string {
key := c.doublyLinkedList.Front().key
fmt.Printf("Evicting by lru strtegy. Evicted Node Key: %s: ", key)
c.doublyLinkedList.RemoveFromFront()
return key
}
func (l *lru) get(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to get operation")
c.doublyLinkedList.MoveNodeToEnd(node)
}
func (l *lru) set(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set operation")
c.doublyLinkedList.AddToEnd(node)
}
func (l *lru) set_overwrite(node *node, value string, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set_overwrite operation")
node.value = value
c.doublyLinkedList.MoveNodeToEnd(node)
}
fifo.go
package main
import "fmt"
type fifo struct {
}
func (l *fifo) evict(c *Cache) string {
fmt.Println("Evicting by fifo strtegy")
key := c.doublyLinkedList.Front().key
c.doublyLinkedList.RemoveFromFront()
return key
}
func (l *fifo) get(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to get operation")
}
func (l *fifo) set(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set operation")
c.doublyLinkedList.AddToEnd(node)
}
func (l *fifo) set_overwrite(node *node, value string, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set_overwrite operation")
}
cache.go
package main
import "fmt"
type Cache struct {
doublyLinkedList *doublyLinkedList
storage map[string]*node
evictionAlgo evictionAlgo
capacity int
maxCapacity int
}
func initCache(evictionAlgo evictionAlgo, maxCapacity int) Cache {
storage := make(map[string]*node)
return Cache{
doublyLinkedList: &doublyLinkedList{},
storage: storage,
evictionAlgo: evictionAlgo,
capacity: 0,
maxCapacity: maxCapacity,
}
}
func (this *Cache) setEvictionAlgo(e evictionAlgo) {
this.evictionAlgo = e
}
func (this *Cache) set(key, value string) {
node_ptr, ok := this.storage[key]
if ok {
this.evictionAlgo.set_overwrite(node_ptr, value, this)
return
}
if this.capacity == this.maxCapacity {
evictedKey := this.evict()
delete(this.storage, evictedKey)
}
node := &node{key: key, value: value}
this.storage[key] = node
this.evictionAlgo.set(node, this)
this.capacity++
}
func (this *Cache) get(key string) string {
node_ptr, ok := this.storage[key]
if ok {
this.evictionAlgo.get(node_ptr, this)
return (*node_ptr).value
}
return ""
}
func (this *Cache) evict() string {
key := this.evictionAlgo.evict(this)
this.capacity--
return key
}
func (this *Cache) print() {
for k, v := range this.storage {
fmt.Printf("key :%s value: %s\n", k, (*v).value)
}
this.doublyLinkedList.TraverseForward()
}
main.go
package main
import "fmt"
func main() {
lru := createEvictioAlgo("lru")
cache := initCache(lru, 3)
cache.set("a", "1")
cache.print()
cache.set("b", "2")
cache.print()
cache.set("c", "3")
cache.print()
value := cache.get("a")
fmt.Printf("key: a, value: %s\n", value)
cache.print()
cache.set("d", "4")
cache.print()
cache.set("e", "5")
cache.print()
}
输出
Shuffling doubly linked list due to set operation
key :a value: 1
key = a, value = 1, prev = <nil>, next = <nil>
Shuffling doubly linked list due to set operation
key :a value: 1
key :b value: 2
key = a, value = 1, prev = <nil>, next = &{b 2 0xc00007e1e0 <nil>}
key = b, value = 2, prev = &{a 1 <nil> 0xc00007e210}, next = <nil>
Shuffling doubly linked list due to set operation
key :a value: 1
key :b value: 2
key :c value: 3
key = a, value = 1, prev = <nil>, next = &{b 2 0xc00007e1e0 0xc00007e2a0}
key = b, value = 2, prev = &{a 1 <nil> 0xc00007e210}, next = &{c 3 0xc00007e210 <nil>}
key = c, value = 3, prev = &{b 2 0xc00007e1e0 0xc00007e2a0}, next = <nil>
Shuffling doubly linked list due to get operation
key: a, value: 1
key :a value: 1
key :b value: 2
key :c value: 3
key = b, value = 2, prev = <nil>, next = &{c 3 0xc00007e210 0xc00007e1e0}
key = c, value = 3, prev = &{b 2 <nil> 0xc00007e2a0}, next = &{a 1 0xc00007e2a0 <nil>}
key = a, value = 1, prev = &{c 3 0xc00007e210 0xc00007e1e0}, next = <nil>
Evicting by lru strtegy. Evicted Node Key: %s: b
Shuffling doubly linked list due to set operation
key :d value: 4
key :c value: 3
key :a value: 1
key = c, value = 3, prev = &{b 2 <nil> 0xc00007e2a0}, next = &{a 1 0xc00007e2a0 0xc00007e450}
key = a, value = 1, prev = &{c 3 0xc00007e210 0xc00007e1e0}, next = &{d 4 0xc00007e1e0 <nil>}
key = d, value = 4, prev = &{a 1 0xc00007e2a0 0xc00007e450}, next = <nil>
Evicting by lru strtegy. Evicted Node Key: %s: c
Shuffling doubly linked list due to set operation
key :a value: 1
key :d value: 4
key :e value: 5
key = a, value = 1, prev = &{c 3 0xc00007e210 0xc00007e1e0}, next = &{d 4 0xc00007e1e0 0xc00007e570}
key = d, value = 4, prev = &{a 1 0xc00007e2a0 0xc00007e450}, next = &{e 5 0xc00007e450 <nil>}
key = e, value = 5, prev = &{d 4 0xc00007e1e0 0xc00007e570}, next = <nil>
结论
这就是设计内存缓存的全部内容。希望你喜欢这篇文章。请在评论中分享反馈。
LRU 缓存设计。
目录
-
概述
-
Set(key int, value int)")
-
Get(key int)")
-
低级设计
-
UML 图
-
程序
-
结论
概述
在本教程中,我们还将考虑在设计中使用 FIFO 方法,除了 LRU 之外,以确保设计在运行时可以灵活地切换到任何算法。
目标是设计一个内存缓存。以下是要求:
-
它应该支持 Set 和 Get 操作。
-
Set 和 Get 操作的时间复杂度为 O(1)。
-
假设缓存的最大容量是 3。一旦缓存已满且需要插入一个新键时,必须从缓存中删除一个现有条目。
-
删除操作应该基于逐出算法 —— FIFO 或 LRU。
-
假设缓存中的键和值的类型为字符串。
-
它应该支持的逐出算法是 先进先出(FIFO) 和 最近最少使用(LRU)。
-
逐出算法应该是可插拔的。这意味着你应该能够在运行时更改逐出算法。
以下是我们设计中的高级内容:
-
我们将有一个 Cache 类,作为与客户端交互的接口。
-
Cache 类将使用 Map 和 双向链表 的组合来存储所有内容。使用这两种数据结构,确保在逐出操作时,Get 和 Set 操作的时间复杂度仍为 O(1)。我们将在本教程后面详细介绍如何实现这一点。
-
Map 将具有类型为字符串的键和类型为指向 双向链表 中节点的指针的值。
-
双向链表 的每个节点将包含键和值。每个节点还会有指向前一个节点的指针以及指向下一个节点的指针,在 双向链表 中。
-
将会有一个 evictionAlgorithm 接口。将会有 LRU 和 FIFO 类实现该 evictionAlgorithm 接口。
-
Cache 类还会嵌入一个 evictionAlgorithm 接口实例。
evictionAlgorithm 接口存在的目的是将 Cache 类与算法解耦,以便我们能够在运行时更改算法。此外,当添加新算法时,Cache 类不应发生变化。这就是 策略设计模式 的应用场景。策略模式建议创建一组算法,每个算法有自己独立的类。这些类遵循相同的接口,使得算法可以在同一家族内互换。假设公共接口名称为 evictionAlgo,那么 FIFO 和 LRU 类将实现此接口。
现在我们的主 Cache 类将嵌入 evictionAlgo 接口。我们的缓存类不会实现所有类型的驱逐算法,而是将所有算法委托给 evictionAlgo 接口。由于 evictionAlgo 是一个接口,我们可以在运行时将算法切换为 LRU 或 FIFO,而无需修改 Cache 类。
同时,我们将使用工厂设计模式来创建每个算法的实例,例如 FIFO、LRU。
让我们来看一下 Get 和 Set 如何在 O(1) 时间内工作。
Set(key int, value int)
对于任何 set 操作,它将首先创建一个包含输入的键和值的双向链表节点。然后会在映射中创建一个条目,其中键是输入的键,值是节点的地址。一旦节点创建完成,就会有两种情况。
-
缓存未满 – 在这种情况下,它将控制权交给当前的 evictionAlgorithm 接口。evictionAlgorithm 接口将根据当前的驱逐算法,将节点插入到双向链表的末尾或前端。这里的每个操作都是 O(1)。
-
缓存已满 – 在这种情况下,它将控制权交给当前的 evictionAlgorithm 接口,根据当前的驱逐算法驱逐一个节点。一旦该节点被驱逐,它将插入新节点。这里的每个操作都是 O(1)。
Get(key int)
对于任何 Get 操作,它首先会检查映射中是否存在给定的键。如果存在,它将获取映射中由该键指向的节点的地址。然后,它会从该节点中获取值。接下来,它会将控制权交给当前的 evictionAlgorithm 接口。evictionAlgorithm 接口将根据当前的驱逐算法,将当前节点在双向链表中移动到末尾或前端。这里的每个操作都是 O(1)。
低级设计
以下是用 Go 编程语言表达的低级设计。稍后我们还将看到 UML 图以及一个工作示例。
缓存类
type cache struct {
dll *doublyLinkedList
storage map[string]string
evictionAlgo evictionAlgo
capacity int
maxCapacity int
}
func (c *cache) setEvictionAlgo(e evictionAlgo)
func (c *cache) add(key, value string)
func (c *cache) get(key string)
func (c *cache) evict()
双向链表
type node struct {
data string
prev *node
next *node
}
type doublyLinkedList struct {
len int
tail *node
head *node
}
func initDoublyList() *doublyLinkedList {
return &doublyLinkedList{}
}
func (d *doublyLinkedList) AddFrontNodeDLL(data string)
func (d *doublyLinkedList) AddEndNodeDLL(data string)
func (d *doublyLinkedList) MoveToFront(node *node) error
func (d *doublyLinkedList) MoveToBack(node *node) error
func (d *doublyLinkedList) Size() int
驱逐算法接口
type evictionAlgo interface {
evict(c *cache)
}
func createEvictioAlgo(algoType string) evictionAlgo
FIFO 算法 – 它实现了驱逐算法接口
type fifo struct {
}
func (l *fifo) evict(c *cache)
LRU 算法 – 它实现了驱逐算法接口
type lru struct {
}
func (l *lru) evict(c *cache)
UML 图

程序
这里是完整的 Go 编程语言代码,如果有任何人感兴趣的话。
doublylinklist.go
package main
import "fmt"
type node struct {
key string
value string
prev *node
next *node
}
type doublyLinkedList struct {
len int
tail *node
head *node
}
func initDoublyList() *doublyLinkedList {
return &doublyLinkedList{}
}
func (d *doublyLinkedList) AddToFront(key, value string) {
newNode := &node{
key: key,
value: value,
}
if d.head == nil {
d.head = newNode
d.tail = newNode
} else {
newNode.next = d.head
d.head.prev = newNode
d.head = newNode
}
d.len++
return
}
func (d *doublyLinkedList) RemoveFromFront() {
if d.head == nil {
return
} else if d.head == d.tail {
d.head = nil
d.tail = nil
} else {
d.head = d.head.next
}
d.len--
}
func (d *doublyLinkedList) AddToEnd(node *node) {
newNode := node
if d.head == nil {
d.head = newNode
d.tail = newNode
} else {
currentNode := d.head
for currentNode.next != nil {
currentNode = currentNode.next
}
newNode.prev = currentNode
currentNode.next = newNode
d.tail = newNode
}
d.len++
}
func (d *doublyLinkedList) Front() *node {
return d.head
}
func (d *doublyLinkedList) MoveNodeToEnd(node *node) {
prev := node.prev
next := node.next
if prev != nil {
prev.next = next
}
if next != nil {
next.prev = prev
}
if d.tail == node {
d.tail = prev
}
if d.head == node {
d.head = next
}
node.next = nil
node.prev = nil
d.len--
d.AddToEnd(node)
}
func (d *doublyLinkedList) TraverseForward() error {
if d.head == nil {
return fmt.Errorf("TraverseError: List is empty")
}
temp := d.head
for temp != nil {
fmt.Printf("key = %v, value = %v, prev = %v, next = %v\n", temp.key, temp.value, temp.prev, temp.next)
temp = temp.next
}
fmt.Println()
return nil
}
func (d *doublyLinkedList) Size() int {
return d.len
}
evictionAlgorithm.go
package main
type evictionAlgo interface {
evict(c *Cache) string
get(node *node, c *Cache)
set(node *node, c *Cache)
set_overwrite(node *node, value string, c *Cache)
}
func createEvictioAlgo(algoType string) evictionAlgo {
if algoType == "fifo" {
return &fifo{}
} else if algoType == "lru" {
return &lru{}
}
return nil
}
lru.go
package main
import "fmt"
type lru struct {
}
func (l *lru) evict(c *Cache) string {
key := c.doublyLinkedList.Front().key
fmt.Printf("Evicting by lru strtegy. Evicted Node Key: %s: ", key)
c.doublyLinkedList.RemoveFromFront()
return key
}
func (l *lru) get(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to get operation")
c.doublyLinkedList.MoveNodeToEnd(node)
}
func (l *lru) set(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set operation")
c.doublyLinkedList.AddToEnd(node)
}
func (l *lru) set_overwrite(node *node, value string, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set_overwrite operation")
node.value = value
c.doublyLinkedList.MoveNodeToEnd(node)
}
fifo.go
package main
import "fmt"
type fifo struct {
}
func (l *fifo) evict(c *Cache) string {
fmt.Println("Evicting by fifo strtegy")
key := c.doublyLinkedList.Front().key
c.doublyLinkedList.RemoveFromFront()
return key
}
func (l *fifo) get(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to get operation")
}
func (l *fifo) set(node *node, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set operation")
c.doublyLinkedList.AddToEnd(node)
}
func (l *fifo) set_overwrite(node *node, value string, c *Cache) {
fmt.Println("Shuffling doubly linked list due to set_overwrite operation")
}
cache.go
package main
import "fmt"
type Cache struct {
doublyLinkedList *doublyLinkedList
storage map[string]*node
evictionAlgo evictionAlgo
capacity int
maxCapacity int
}
func initCache(evictionAlgo evictionAlgo, maxCapacity int) Cache {
storage := make(map[string]*node)
return Cache{
doublyLinkedList: &doublyLinkedList{},
storage: storage,
evictionAlgo: evictionAlgo,
capacity: 0,
maxCapacity: maxCapacity,
}
}
func (this *Cache) setEvictionAlgo(e evictionAlgo) {
this.evictionAlgo = e
}
func (this *Cache) set(key, value string) {
node_ptr, ok := this.storage[key]
if ok {
this.evictionAlgo.set_overwrite(node_ptr, value, this)
return
}
if this.capacity == this.maxCapacity {
evictedKey := this.evict()
delete(this.storage, evictedKey)
}
node := &node{key: key, value: value}
this.storage[key] = node
this.evictionAlgo.set(node, this)
this.capacity++
}
func (this *Cache) get(key string) string {
node_ptr, ok := this.storage[key]
if ok {
this.evictionAlgo.get(node_ptr, this)
return (*node_ptr).value
}
return ""
}
func (this *Cache) evict() string {
key := this.evictionAlgo.evict(this)
this.capacity--
return key
}
func (this *Cache) print() {
for k, v := range this.storage {
fmt.Printf("key :%s value: %s\n", k, (*v).value)
}
this.doublyLinkedList.TraverseForward()
}
main.go
package main
import "fmt"
func main() {
lru := createEvictioAlgo("lru")
cache := initCache(lru, 3)
cache.set("a", "1")
cache.print()
cache.set("b", "2")
cache.print()
cache.set("c", "3")
cache.print()
value := cache.get("a")
fmt.Printf("key: a, value: %s\n", value)
cache.print()
cache.set("d", "4")
cache.print()
cache.set("e", "5")
cache.print()
}
输出
Shuffling doubly linked list due to set operation
key :a value: 1
key = a, value = 1, prev = <nil>, next = <nil>
Shuffling doubly linked list due to set operation
key :a value: 1
key :b value: 2
key = a, value = 1, prev = <nil>, next = &{b 2 0xc00007e1e0 <nil>}
key = b, value = 2, prev = &{a 1 <nil> 0xc00007e210}, next = <nil>
Shuffling doubly linked list due to set operation
key :a value: 1
key :b value: 2
key :c value: 3
key = a, value = 1, prev = <nil>, next = &{b 2 0xc00007e1e0 0xc00007e2a0}
key = b, value = 2, prev = &{a 1 <nil> 0xc00007e210}, next = &{c 3 0xc00007e210 <nil>}
key = c, value = 3, prev = &{b 2 0xc00007e1e0 0xc00007e2a0}, next = <nil>
Shuffling doubly linked list due to get operation
key: a, value: 1
key :a value: 1
key :b value: 2
key :c value: 3
key = b, value = 2, prev = <nil>, next = &{c 3 0xc00007e210 0xc00007e1e0}
key = c, value = 3, prev = &{b 2 <nil> 0xc00007e2a0}, next = &{a 1 0xc00007e2a0 <nil>}
key = a, value = 1, prev = &{c 3 0xc00007e210 0xc00007e1e0}, next = <nil>
Evicting by lru strtegy. Evicted Node Key: %s: b
Shuffling doubly linked list due to set operation
key :d value: 4
key :c value: 3
key :a value: 1
key = c, value = 3, prev = &{b 2 <nil> 0xc00007e2a0}, next = &{a 1 0xc00007e2a0 0xc00007e450}
key = a, value = 1, prev = &{c 3 0xc00007e210 0xc00007e1e0}, next = &{d 4 0xc00007e1e0 <nil>}
key = d, value = 4, prev = &{a 1 0xc00007e2a0 0xc00007e450}, next = <nil>
Evicting by lru strtegy. Evicted Node Key: %s: c
Shuffling doubly linked list due to set operation
key :a value: 1
key :d value: 4
key :e value: 5
key = a, value = 1, prev = &{c 3 0xc00007e210 0xc00007e1e0}, next = &{d 4 0xc00007e1e0 0xc00007e570}
key = d, value = 4, prev = &{a 1 0xc00007e2a0 0xc00007e450}, next = &{e 5 0xc00007e450 <nil>}
key = e, value = 5, prev = &{d 4 0xc00007e1e0 0xc00007e570}, next = <nil>
结论
这就是设计内存缓存的全部内容。希望你喜欢这篇文章。如果有任何反馈,请在评论中分享。
设计一个自动售货机
概述
目标是设计一个自动售货机。请注意,设计自动售货机是一个面向对象的问题,而不是一个分布式系统问题。因此,我们需要以这种方式来处理它。以下是需求:
-
自动售货机会允许用户选择物品
-
自动售货机会允许管理员添加物品
-
一旦物品被选中,用户就可以投入金钱。物品将在输入金钱后被发放
-
自动售货机将有不同的状态。
为了简化,假设自动售货机只有一种物品或产品。同时,假设自动售货机可以处于 4 种不同的状态。
-
hasItem
-
noItem
-
itemRequested
-
hasMoney
自动售货机还将有不同的操作。为了简化,假设只有四个操作:
*** 选择物品
-
添加物品
-
投入金钱
-
发放物品
我们可以在这里使用状态设计模式来设计自动售货机。状态设计模式是一种基于有限状态机的行为设计模式。
现在问题是,为什么我们要使用状态设计模式来设计自动售货机?以下是两个原因:
-
状态设计模式用于当一个对象可以处于多种不同的状态时。根据当前请求,对象需要改变其当前状态。
- 自动售货机可以处于多种不同的状态。自动售货机会从一种状态转移到另一种状态。假设自动售货机处于itemRequested状态,然后在执行“插入金钱”操作后,它将转移到hasMoney状态。
-
状态设计模式用于当一个对象对相同的请求根据当前状态给出不同的响应时。在这里使用状态设计模式可以避免大量的条件语句。
- 例如,在自动售货机的情况下,如果用户想购买物品,机器将在处于hasItemState状态时继续操作,而在处于noItemState状态时拒绝。如果你注意到,在购买物品的请求下,自动售货机会根据是否处于hasItemState或noItemState给出两种不同的响应。通过这种方式,我们的代码中不会有任何条件语句。所有逻辑都由具体的状态实现处理。
UML 图
以下是自动售货机的 UML 图:

这是整体思路:
-
我们将拥有一个接口“State”,它定义了表示自动售货机中动作的函数签名。以下是这些动作的函数签名:
-
addItem(int) error
-
requestItem() error
-
insertMoney(money int) error
-
dispenseItem() error
-
-
每个具体状态都实现了上述四个函数,并根据这些操作转移到其他状态或给出一些响应。
-
每个具体的状态还会嵌入一个指向当前自动售货机对象的指针,这样状态转换可以在该对象上进行。
低级设计
以下是用 Go 编程语言表达的低级设计,稍后我们还会看到一个实际的例子
自动售货机类
type vendingMachine struct {
hasItem state
itemRequested state
hasMoney state
noItem state
currentState state
itemCount int
itemPrice int
}
func (v *vendingMachine) requestItem() error
func (v *vendingMachine) addItem(count int) error
func (v *vendingMachine) insertMoney(money int) error
func (v *vendingMachine) dispenseItem() error
func (v *vendingMachine) setState(s state)
func (v *vendingMachine) incrementItemCount(count int)
状态接口
type state interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}
无商品状态类
type noItemState struct {
vendingMachine *vendingMachine
}
func (i *noItemState) requestItem() error
func (i *noItemState) addItem(count int) error
func (i *noItemState) insertMoney(money int) error
func (i *noItemState) dispenseItem() error
请求商品状态类
type itemRequestedState struct {
vendingMachine *vendingMachine
}
func (i *itemRequestedState) requestItem() error
func (i *itemRequestedState) addItem(count int) error
func (i *itemRequestedState) insertMoney(money int) error
func (i *itemRequestedState) dispenseItem() error
有商品状态类
type hasItemState struct {
vendingMachine *vendingMachine
}
func (i *hasItemState) requestItem() error
func (i *hasItemState) addItem(count int) error
func (i *hasItemState) insertMoney(money int) error
func (i *hasItemState) dispenseItem() error
有钱状态类
type hasMoneyState struct {
vendingMachine *vendingMachine
}
func (i *hasMoneyState) requestItem() error
func (i *hasMoneyState) addItem(count int) error
func (i *hasMoneyState) insertMoney(money int) error
func (i *hasMoneyState) dispenseItem() error
程序
如果有人对 Go 编程语言感兴趣,这里是完整的工作代码
vendingMachine.go
package main
import "fmt"
type vendingMachine struct {
hasItem state
itemRequested state
hasMoney state
noItem state
currentState state
itemCount int
itemPrice int
}
func newVendingMachine(itemCount, itemPrice int) *vendingMachine {
v := &vendingMachine{
itemCount: itemCount,
itemPrice: itemPrice,
}
hasItemState := &hasItemState{
vendingMachine: v,
}
itemRequestedState := &itemRequestedState{
vendingMachine: v,
}
hasMoneyState := &hasMoneyState{
vendingMachine: v,
}
noItemState := &noItemState{
vendingMachine: v,
}
v.setState(hasItemState)
v.hasItem = hasItemState
v.itemRequested = itemRequestedState
v.hasMoney = hasMoneyState
v.noItem = noItemState
return v
}
func (v *vendingMachine) requestItem() error {
return v.currentState.requestItem()
}
func (v *vendingMachine) addItem(count int) error {
return v.currentState.addItem(count)
}
func (v *vendingMachine) insertMoney(money int) error {
return v.currentState.insertMoney(money)
}
func (v *vendingMachine) dispenseItem() error {
return v.currentState.dispenseItem()
}
func (v *vendingMachine) setState(s state) {
v.currentState = s
}
func (v *vendingMachine) incrementItemCount(count int) {
fmt.Printf("Adding %d items\n", count)
v.itemCount = v.itemCount + count
}
state.go
package main
type state interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}
noItemState.go
package main
import "fmt"
type noItemState struct {
vendingMachine *vendingMachine
}
func (i *noItemState) requestItem() error {
return fmt.Errorf("Item out of stock")
}
func (i *noItemState) addItem(count int) error {
i.vendingMachine.incrementItemCount(count)
i.vendingMachine.setState(i.vendingMachine.hasItem)
return nil
}
func (i *noItemState) insertMoney(money int) error {
return fmt.Errorf("Item out of stock")
}
func (i *noItemState) dispenseItem() error {
return fmt.Errorf("Item out of stock")
}
itemRequestedState.go
package main
import "fmt"
type itemRequestedState struct {
vendingMachine *vendingMachine
}
func (i *itemRequestedState) requestItem() error {
return fmt.Errorf("Item already requested")
}
func (i *itemRequestedState) addItem(count int) error {
return fmt.Errorf("Item Dispense in progress")
}
func (i *itemRequestedState) insertMoney(money int) error {
if money < i.vendingMachine.itemPrice {
fmt.Errorf("Inserted money is less. Please insert %d", i.vendingMachine.itemPrice)
}
fmt.Println("Money entered is ok")
i.vendingMachine.setState(i.vendingMachine.hasMoney)
return nil
}
func (i *itemRequestedState) dispenseItem() error {
return fmt.Errorf("Please insert money first")
}
hasItemState.go
package main
import "fmt"
type hasItemState struct {
vendingMachine *vendingMachine
}
func (i *hasItemState) requestItem() error {
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItem)
return fmt.Errorf("No item present")
}
fmt.Printf("Item requestd\n")
i.vendingMachine.setState(i.vendingMachine.itemRequested)
return nil
}
func (i *hasItemState) addItem(count int) error {
fmt.Printf("%d items added\n", count)
i.vendingMachine.incrementItemCount(count)
return nil
}
func (i *hasItemState) insertMoney(money int) error {
return fmt.Errorf("Please select item first")
}
func (i *hasItemState) dispenseItem() error {
return fmt.Errorf("Please select item first")
}
hasMoneyState.go
package main
import "fmt"
type hasMoneyState struct {
vendingMachine *vendingMachine
}
func (i *hasMoneyState) requestItem() error {
return fmt.Errorf("Item dispense in progress")
}
func (i *hasMoneyState) addItem(count int) error {
return fmt.Errorf("Item dispense in progress")
}
func (i *hasMoneyState) insertMoney(money int) error {
return fmt.Errorf("Item out of stock")
}
func (i *hasMoneyState) dispenseItem() error {
fmt.Println("Dispensing Item")
i.vendingMachine.itemCount = i.vendingMachine.itemCount - 1
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItem)
} else {
i.vendingMachine.setState(i.vendingMachine.hasItem)
}
return nil
}
main.go
package main
import (
"fmt"
"log"
)
func main() {
vendingMachine := newVendingMachine(1, 10)
err := vendingMachine.requestItem()
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.insertMoney(10)
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.dispenseItem()
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println()
err = vendingMachine.addItem(2)
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println()
err = vendingMachine.requestItem()
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.insertMoney(10)
if err != nil {
log.Fatalf(err.Error())
}
err = vendingMachine.dispenseItem()
if err != nil {
log.Fatalf(err.Error())
}
}
输出:
Item requestd
Money entered is ok
Dispensing Item
Adding 2 items
Item requestd
Money entered is ok
Dispensing Item
```**
# 自动取款机系统设计
> 原文:[`techbyexample.com/atm-machine-system-design/`](https://techbyexample.com/atm-machine-system-design/)
目录
+ 概述
+ UML 图
+ 低级设计
+ 程序
+ 结论
## **概述**
目标是设计一个自动取款机。请注意,设计自动取款机是一个面向对象的问题,而不是分布式系统问题。因此我们需要以这种方式来处理。下面是需求。
+ 自动取款机将允许管理员或银行添加资金
+ 自动取款机将允许用户输入金额
+ 一旦输入金额,用户可以输入密码。密码正确后,现金将被发放。
+ 自动取款机将具有不同的状态。
同样,为了简单起见,假设自动取款机可以处于四种不同的状态。
+ **noMoney –** 这是自动取款机没有资金时的初始状态,因此无法接受提款。
+ **hasMoney –** 这是自动取款机准备接受提款时的下一个状态。
+ **amountEntered –** 这是用户输入金额后的状态。在此状态下,它将允许用户输入金额。
+ **pinEntered –** 这是用户输入密码后的状态。如果密码正确,则现金将被发放。
**自动取款机还会有不同的动作。为了简单起见,假设只有四个动作:**
*** **添加资金**
+ **输入金额**
+ **输入密码**
+ **发放现金**
我们可以在这里使用状态设计模式来设计自动取款机。状态设计模式是一种基于有限状态机的行为设计模式。
那么问题是,为什么我们要使用状态设计模式来设计自动取款机?下面是两个原因。
+ 状态设计模式用于当对象可以处于多种不同的状态时。根据当前的请求,对象需要改变其当前状态。
+ 自动取款机可以处于多个不同的状态。假设自动取款机当前处于**amountEntered**状态,那么一旦执行了**“Enter Pin”**操作,它将转移到**pinEntered**状态。
+ 状态设计模式用于当对象在不同的状态下对相同的请求有不同响应时。使用状态设计模式可以避免许多条件语句。
+ 例如,在自动取款机(ATM)的情况下,如果用户想要取款,则机器将在**hasMoney**状态下继续操作,或者如果处于**noMoney**状态则拒绝操作。如果你注意到这里,自动取款机在取款请求时,根据是否处于**hasMoney**或**noMoney**状态,会给出两种不同的响应。通过这种方式,我们的代码将不会有任何条件语句。所有的逻辑都由具体的状态实现来处理。
## **UML 图**
以下是 ATM 机器的 UML 图

这是整体的想法
+ 我们将有一个接口“State”,它定义了表示 ATM 机器上下文中的操作的函数签名。以下是操作函数签名
1. stateName() string
1. addMoney(int) error
1. enterAmount(int) error
1. enterPin(money int) error
1. dispenseCash() error
+ 每个具体状态都实现了上述所有 5 个功能,并根据这些操作要么转移到另一个状态,要么做出相应的响应。
+ 每个具体状态还嵌入了指向当前 ATM 机器对象的指针,以便状态转换可以调用 ATM 机器对象上的某些方法。
## **低**–**级设计**
以下是用 Go 编程语言表达的低级设计。
稍后我们还会看到一个工作示例
**ATM 机器类**
```go
type atmMachine struct {
hasMoney state
noMoney state
amountEntered state
pinEntered state
currentState state
totalMoney int
}
func newATMMachine(totalMoney int) *atmMachine {
a := &atmMachine{
totalMoney: totalMoney,
}
hasMoneyState := &hasMoneyState{
atmMachine: a,
}
noMoneyState := &noMoneyState{
atmMachine: a,
}
amountEnteredState := &amountEnteredState{
atmMachine: a,
}
pinEnteredState := &pinEnteredState{
atmMachine: a,
}
a.setState(hasMoneyState)
a.hasMoney = hasMoneyState
a.noMoney = noMoneyState
a.amountEntered = amountEnteredState
a.pinEntered = pinEnteredState
return a
}
func (v *atmMachine) addMoney(money int) error
func (v *atmMachine) enterAmount(money int) error
func (v *atmMachine) enterPin(money int) error
func (v *atmMachine) dispenseCash(money int) error
func (v *atmMachine) setState(s state)
func (v *atmMachine) incrementMoney(money int)
func (v *atmMachine) decrementMoney(money int)
func (v *atmMachine) checkAvailability(money int) error
func (v *atmMachine) verifyPin(pin int) error
状态接口
type state interface {
stateName() string
addMoney(int) error
enterAmount(int) error
enterPin(int) error
dispenseCash(int) error
}
NoMoney 状态类
type noMoneyState struct {
atmMachine *atmMachine
}
func (i *noMoneyState) stateName() string
func (i *noMoneyState) addMoney(money int) error
func (i *noMoneyState) enterAmount(money int) error
func (i *noMoneyState) enterPin(pin int) error
func (i *noMoneyState) dispenseCash(money int) error
Has Money 状态类
type hasMoneyState struct {
atmMachine *atmMachine
}
func (i *hasMoneyState) stateName() string
func (i *hasMoneyState) addMoney(money int) error
func (i *hasMoneyState) enterAmount(money int) error
func (i *hasMoneyState) enterPin(pin int) error
func (i *hasMoneyState) dispenseCash(money int) error
Amount 输入状态类
type amountEnteredState struct {
atmMachine *atmMachine
}
func (i *amountEnteredState) stateName() string
func (i *amountEnteredState) addMoney(money int) error
func (i *amountEnteredState) enterAmount(money int) error
func (i *amountEnteredState) enterPin(pin int) error
func (i *amountEnteredState) dispenseCash(money int) error
密码输入状态类
type pinEnteredState struct {
atmMachine *atmMachine
}
func (i *pinEnteredState) stateName() string
func (i *pinEnteredState) addMoney(money int) error
func (i *pinEnteredState) enterAmount(money int) error
func (i *pinEnteredState) enterPin(pin int) error
func (i *pinEnteredState) dispenseCash(money int) error
程序
这里是完整的工作代码,如果有人对 Go 编程语言感兴趣的话
atmMachine.go
package main
import "fmt"
type atmMachine struct {
hasMoney state
noMoney state
amountEntered state
pinEntered state
currentState state
totalMoney int
}
func newATMMachine(totalMoney int) *atmMachine {
a := &atmMachine{
totalMoney: totalMoney,
}
hasMoneyState := &hasMoneyState{
atmMachine: a,
}
noMoneyState := &noMoneyState{
atmMachine: a,
}
amountEnteredState := &amountEnteredState{
atmMachine: a,
}
pinEnteredState := &pinEnteredState{
atmMachine: a,
}
a.setState(hasMoneyState)
a.hasMoney = hasMoneyState
a.noMoney = noMoneyState
a.amountEntered = amountEnteredState
a.pinEntered = pinEnteredState
return a
}
func (v *atmMachine) addMoney(money int) error {
return v.currentState.addMoney(money)
}
func (v *atmMachine) enterAmount(money int) error {
return v.currentState.enterAmount(money)
}
func (v *atmMachine) enterPin(money int) error {
return v.currentState.enterPin(money)
}
func (v *atmMachine) dispenseCash(money int) error {
return v.currentState.dispenseCash(money)
}
func (v *atmMachine) setState(s state) {
v.currentState = s
}
func (v *atmMachine) incrementMoney(money int) {
fmt.Printf("Adding %d money:\n", money)
v.totalMoney = v.totalMoney + money
}
func (v *atmMachine) decrementMoney(money int) {
fmt.Printf("Dispensing %d cash:\n", money)
v.totalMoney = v.totalMoney - money
}
func (v *atmMachine) checkAvailability(money int) error {
fmt.Printf("Checking Availability\n")
if money < v.totalMoney {
return nil
}
return fmt.Errorf("Not enough money")
}
func (v *atmMachine) verifyPin(pin int) error {
fmt.Println("Verifying Pin")
//Pin is always true
return nil
}
state.go
package main
type state interface {
stateName() string
addMoney(int) error
enterAmount(int) error
enterPin(int) error
dispenseCash(int) error
}
noMoneyState.go
package main
import "fmt"
type noMoneyState struct {
atmMachine *atmMachine
}
func (i *noMoneyState) stateName() string {
return "noMoneyState"
}
func (i *noMoneyState) addMoney(money int) error {
fmt.Errorf("Add money in progress")
i.atmMachine.incrementMoney(money)
return nil
}
func (i *noMoneyState) enterAmount(money int) error {
return fmt.Errorf("Add money first")
}
func (i *noMoneyState) enterPin(pin int) error {
return fmt.Errorf("Add money first")
}
func (i *noMoneyState) dispenseCash(money int) error {
return fmt.Errorf("Add money first")
}
hasMoneyState.go
package main
import "fmt"
type hasMoneyState struct {
atmMachine *atmMachine
}
func (i *hasMoneyState) stateName() string {
return "hasMoneyState"
}
func (i *hasMoneyState) addMoney(money int) error {
fmt.Errorf("Add money in progress")
i.atmMachine.incrementMoney(money)
return nil
}
func (i *hasMoneyState) enterAmount(money int) error {
fmt.Errorf("Amount is entered. Amount:%n", money)
err := i.atmMachine.checkAvailability(money)
if err != nil {
return err
}
i.atmMachine.setState(i.atmMachine.amountEntered)
return nil
}
func (i *hasMoneyState) enterPin(pin int) error {
return fmt.Errorf("First enter the amount")
}
func (i *hasMoneyState) dispenseCash(money int) error {
return fmt.Errorf("First enter the amount")
}
amountEnteredState.go
package main
import "fmt"
type amountEnteredState struct {
atmMachine *atmMachine
}
func (i *amountEnteredState) stateName() string {
return "amountEnteredState"
}
func (i *amountEnteredState) addMoney(money int) error {
return fmt.Errorf("Dispensing process in progress")
}
func (i *amountEnteredState) enterAmount(money int) error {
return fmt.Errorf("Amount already entered")
}
func (i *amountEnteredState) enterPin(pin int) error {
err := i.atmMachine.verifyPin(pin)
if err != nil {
return err
}
i.atmMachine.setState(i.atmMachine.pinEntered)
return nil
}
func (i *amountEnteredState) dispenseCash(money int) error {
return fmt.Errorf("First Enter Pin")
}
pinEnteredState.go
package main
import "fmt"
type pinEnteredState struct {
atmMachine *atmMachine
}
func (i *pinEnteredState) stateName() string {
return "pinEnteredState"
}
func (i *pinEnteredState) addMoney(money int) error {
return fmt.Errorf("Dispensing process in progress")
}
func (i *pinEnteredState) enterAmount(money int) error {
return fmt.Errorf("Amount and pin already entered")
}
func (i *pinEnteredState) enterPin(pin int) error {
return fmt.Errorf("Pin already entered")
}
func (i *pinEnteredState) dispenseCash(money int) error {
i.atmMachine.decrementMoney(money)
i.atmMachine.setState(i.atmMachine.hasMoney)
return nil
}
main.go
package main
import (
"fmt"
"log"
)
func main() {
atmMachine := newATMMachine(100)
fmt.Println("<<<<<First Transactin: Withdrawing amount 10>>>> ")
fmt.Printf("ATM current state %s\n\n", atmMachine.currentState.stateName())
err := atmMachine.enterAmount(10)
if err != nil {
log.Fatalf(err.Error())
}
fmt.Printf("Amount Entered: %d\n", 10)
fmt.Printf("Atm Total Money: %d\n", atmMachine.totalMoney)
fmt.Printf("ATM current state %s\n\n", atmMachine.currentState.stateName())
err = atmMachine.enterPin(1234)
if err != nil {
log.Fatalf(err.Error())
}
fmt.Printf("Pin Entered: %d\n", 10)
fmt.Printf("ATM Total Money: %d\n", atmMachine.totalMoney)
fmt.Printf("ATM current state %s\n\n", atmMachine.currentState.stateName())
err = atmMachine.dispenseCash(10)
if err != nil {
log.Fatalf(err.Error())
}
fmt.Printf("Dispense Cash: %d\n", 10)
fmt.Printf("ATM Total Money: %d\n", atmMachine.totalMoney)
fmt.Printf("ATM current state %s\n\n", atmMachine.currentState.stateName())
fmt.Println()
fmt.Println("<<<<<Second Transactin: Admin adding 50>>>>")
err = atmMachine.addMoney(50)
if err != nil {
log.Fatalf(err.Error())
}
fmt.Printf("Atm Total Money: %d\n", atmMachine.totalMoney)
fmt.Printf("ATM current state %s\n\n", atmMachine.currentState.stateName())
fmt.Println("<<<<<Third Transaction. Withdrawing amount 200>>>>")
err = atmMachine.enterAmount(200)
if err != nil {
log.Fatalf(err.Error())
}
}
输出:
<<<<<First Transactin: Withdrawing amount 10>>>>
ATM current state hasMoneyState
Checking Availability
Amount Entered: 10
Atm Total Money: 100
ATM current state amountEnteredState
Verifying Pin
Pin Entered: 10
ATM Total Money: 100
ATM current state pinEnteredState
Dispensing 10 cash:
Dispense Cash: 10
ATM Total Money: 90
ATM current state hasMoneyState
<<<<<Second Transactin: Admin adding 50>>>>
Adding 50 money:
Atm Total Money: 140
ATM current state hasMoneyState
<<<<<Third Transaction. Withdrawing amount 200>>>>
Checking Availability
2021/11/26 18:29:26 Not enough money
exit status 1
结论
这就是设计 ATM 机器的全部内容。希望你喜欢这篇文章。请在评论中分享反馈
井字棋游戏的系统设计
目录
-
概述
-
UML 图
-
低级设计
-
程序
-
完整工作代码:
-
结论
概述
井字棋是一种棋盘游戏。首先让我们理解游戏规则。在本教程中,我们将
-
首先通过一个例子了解井字棋游戏
-
查看低级设计的 UML 图
-
然后我们将看到用 Go 语言表达的低级设计
-
最后,我们将看到一个具有适当设计的完整工作代码
首先通过一个例子了解井字棋游戏
-
这是一个 n*n 的棋盘,每个块只能标记为十字或圆圈,前提是该块为空。
-
两个玩家同时进行游戏,每个玩家轮流行动。
-
第一玩家在其回合时在棋盘上的任意一个空块上标记一个十字。而第二玩家在其回合时在棋盘上的任意一个空块上标记一个圆圈。
-
游戏的目标是让某一方在一整行、一整列或一条对角线上占满任意一个符号,十字或圆圈。
-
两个玩家将尽力阻止对方实现目标。谁先实现目标,谁就获胜。
-
如果棋盘上的所有块都已满,且没有任何玩家能够用其符号标记整行、整列或对角线,则游戏结果为平局。
-
一旦有一方获胜,游戏将不再允许任何移动。
让我们通过一个例子来理解这个游戏。假设是一个 3*3 的网格。点(‘.’)表示一个空白块。
Player 1 Move with Symbol * at Position X:1 Y:1
...
.*.
...
Player 2 Move with Symbol o at Position X:1 Y:2
...
.*o
...
Player 1 Move with Symbol * at Position X:2 Y:0
...
.*o
*..
Player 2 Move with Symbol o at Position X:0 Y:2
..o
.*o
*..
Player 1 Move with Symbol * at Position X:2 Y:2
..o
.*o
*.*
Player 2 Move with Symbol o at Position X:0 Y:0
o.o
.*o
*.*
Player 1 Move with Symbol * at Position X:2 Y:1
o.o
.*o
***
First Player Win
o.o
.*o
***
在上面的游戏中,第一玩家获胜,因为第三行全部被十字符号‘*’占据。
UML 图
让我们先看看同样的 UML 图。

以下是 UML 图中的一些思路
-
有一个符号枚举(Symbol Enum),表示棋盘上使用的不同符号。符号可以是十字、圆圈或点。点表示棋盘中的一个空白块。
-
有一个 iPlayer 接口,表示游戏中的玩家。
-
玩家可以是人类玩家或计算机玩家。因此,有一个humanPlayer类和一个computerPlayer类,它们实现了 iPlayer 接口。
-
有一个棋盘类,仅用于捕获棋盘的细节。根据棋盘的状态,它会告诉你是否有某一行、列或对角线已经被完成,并且是哪个符号。它与玩家或游戏本身无关。它知道如何打印棋盘。这个棋盘类将会被游戏类使用,正如我们接下来所见。
-
有一个游戏状态枚举类(Game Status Enum),定义了游戏的不同状态。游戏可以处于进行中(Inprogresss)、平局(Draw)、第一玩家胜(FirstPlayerWin)或第二玩家胜(SecondPlayerWin)状态。
-
有一个游戏类用于控制游戏的执行。它包含了棋盘对象,以及两个玩家对象作为其字段。除此之外,它还有其他字段,如 gameStatus(游戏状态)、moveIndex(步数索引)等。
低级设计
让我们来看一下用 Go 编程语言表达的该问题的低级设计。
iPlayer 接口
type iPlayer interface {
getSymbol() Symbol
getNextMove() (int, int, error)
getID() int
}
人类玩家类
type humanPlayer struct {
symbol Symbol
}
func (h *humanPlayer) getSymbol() Symbol
func (h *humanPlayer) getNextMove() (int, int, error)
func (h *humanPlayer) getID() int
计算机玩家类
type computerPlayer struct {
symbol Symbol
id int
}
func (c *computerPlayer) getSymbol() Symbol
func (c *computerPlayer) getNextMove() (int, int, error)
func (c *computerPlayer) getID() int
符号枚举
type Symbol uint8
const (
Cross Symbol = iota
Circle
Dot
)
游戏状态枚举
type GameStatus uint8
const (
GameInProgress GameStatus = iota
GameDraw
FirstPlayerWin
SecondPlayerWin
)
棋盘类
type board struct {
square [][]Symbol
dimension int
}
func (b *board) markSymbol(i, j int, symbol Symbol) (bool, Symbol, error)
func (b *board) checkWin(i, j int, symbol Symbol) bool
func (b *board) printBoard()
游戏类
type game struct {
board *board
firstPlayer iPlayer
secondPlayer iPlayer
firstPlayerTurn bool
moveIndex int
gameStatus GameStatus
}
func initGame(b *board, p1, p2 iPlayer) *game
func (g *game) play() error
func (g *game) setGameStatus(win bool, symbol Symbol)
func (g *game) printMove(player iPlayer, x, y int)
func (g *game) printResult()
程序
这是完整的工作代码。
symbol.go
package main
type Symbol uint8
const (
Cross Symbol = iota
Circle
Dot
)
iPlayer.go
package main
type iPlayer interface {
getSymbol() Symbol
getNextMove() (int, int, error)
getID() int
}
humanPlayer.go
package main
import "fmt"
var (
MovesPlayer1 = [4][2]int{{1, 1}, {2, 0}, {2, 2}, {2, 1}}
MovesPlayer2 = [4][2]int{{1, 2}, {0, 2}, {0, 0}, {0, 0}}
)
type humanPlayer struct {
symbol Symbol
index int
id int
}
func (h *humanPlayer) getSymbol() Symbol {
return h.symbol
}
func (h *humanPlayer) getNextMove() (int, int, error) {
if h.symbol == Cross {
h.index = h.index + 1
return MovesPlayer1[h.index-1][0], MovesPlayer1[h.index-1][1], nil
} else if h.symbol == Circle {
h.index = h.index + 1
return MovesPlayer2[h.index-1][0], MovesPlayer2[h.index-1][1], nil
}
return 0, 0, fmt.Errorf("Invalid Symbol")
}
func (h *humanPlayer) getID() int {
return h.id
}
computerPlayer.go
package main
type computerPlayer struct {
symbol Symbol
id int
}
func (c *computerPlayer) getSymbol() Symbol {
return c.symbol
}
func (c *computerPlayer) getNextMove() (int, int, error) {
//To be implemented
return 0, 0, nil
}
func (c *computerPlayer) getID() int {
return c.id
}
gameStatus.go
package main
type GameStatus uint8
const (
GameInProgress GameStatus = iota
GameDraw
FirstPlayerWin
SecondPlayerWin
)
board.go
package main
import "fmt"
type board struct {
square [][]Symbol
dimension int
}
func (b *board) printBoard() {
for i := 0; i < b.dimension; i++ {
for j := 0; j < b.dimension; j++ {
if b.square[i][j] == Dot {
fmt.Print(".")
} else if b.square[i][j] == Cross {
fmt.Print("*")
} else {
fmt.Print("o")
}
}
fmt.Println("")
}
}
func (b *board) markSymbol(i, j int, symbol Symbol) (bool, Symbol, error) {
if i > b.dimension || j > b.dimension {
return false, Dot, fmt.Errorf("index input is greater than dimension")
}
if b.square[i][j] != Dot {
return false, Dot, fmt.Errorf("input square already marked")
}
if symbol != Cross && symbol != Circle {
return false, Dot, fmt.Errorf("incorrect Symbol")
}
b.square[i][j] = symbol
win := b.checkWin(i, j, symbol)
return win, symbol, nil
}
func (b *board) checkWin(i, j int, symbol Symbol) bool {
//Check Row
rowMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[i][k] != symbol {
rowMatch = false
}
}
if rowMatch {
return rowMatch
}
//Check Row
columnMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[k][j] != symbol {
columnMatch = false
}
}
if columnMatch {
return columnMatch
}
//Check diagonal
diagonalMatch := false
if i == j {
diagonalMatch = true
for k := 0; k < b.dimension; k++ {
if b.square[k][k] != symbol {
diagonalMatch = false
}
}
}
return diagonalMatch
}
game.go
package main
import "fmt"
type game struct {
board *board
firstPlayer iPlayer
secondPlayer iPlayer
firstPlayerTurn bool
moveIndex int
gameStatus GameStatus
}
func initGame(b *board, p1, p2 iPlayer) *game {
game := &game{
board: b,
firstPlayer: p1,
secondPlayer: p2,
firstPlayerTurn: true,
gameStatus: GameInProgress,
}
return game
}
func (g *game) play() error {
var win bool
var symbol Symbol
for {
if g.firstPlayerTurn {
x, y, err := g.firstPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.firstPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = false
g.printMove(g.firstPlayer, x, y)
} else {
x, y, err := g.secondPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.secondPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = true
g.printMove(g.secondPlayer, x, y)
}
g.moveIndex = g.moveIndex + 1
g.setGameStatus(win, symbol)
if g.gameStatus != GameInProgress {
break
}
}
return nil
}
func (g *game) setGameStatus(win bool, symbol Symbol) {
if win {
if g.firstPlayer.getSymbol() == symbol {
g.gameStatus = FirstPlayerWin
return
} else if g.secondPlayer.getSymbol() == symbol {
g.gameStatus = SecondPlayerWin
return
}
}
if g.moveIndex == g.board.dimension*g.board.dimension {
g.gameStatus = GameDraw
return
}
g.gameStatus = GameInProgress
}
func (g *game) printMove(player iPlayer, x, y int) {
symbolString := ""
symbol := player.getSymbol()
if symbol == Cross {
symbolString = "*"
} else if symbol == Circle {
symbolString = "o"
}
fmt.Printf("Player %d Move with Symbol %s at Position X:%d Y:%d\n", player.getID(), symbolString, x, y)
g.board.printBoard()
fmt.Println("")
}
func (g *game) printResult() {
switch g.gameStatus {
case GameInProgress:
fmt.Println("Game in Between")
case GameDraw:
fmt.Println("Game Drawn")
case FirstPlayerWin:
fmt.Println("First Player Win")
case SecondPlayerWin:
fmt.Println("Second Player Win")
default:
fmt.Println("Invalid Game Status")
}
g.board.printBoard()
}
输出
在上面的程序中,我们已经在humanPlayer.go文件中固定了两个玩家的步伐。基于这些步伐,以下是输出结果。
Player 1 Move with Symbol * at Position X:1 Y:1
...
.*.
...
Player 2 Move with Symbol o at Position X:1 Y:2
...
.*o
...
Player 1 Move with Symbol * at Position X:2 Y:0
...
.*o
*..
Player 2 Move with Symbol o at Position X:0 Y:2
..o
.*o
*..
Player 1 Move with Symbol * at Position X:2 Y:2
..o
.*o
*.*
Player 2 Move with Symbol o at Position X:0 Y:0
o.o
.*o
*.*
Player 1 Move with Symbol * at Position X:2 Y:1
o.o
.*o
***
First Player Win
o.o
.*o
***
完整工作代码:
这是一个文件中的完整工作代码。
main.go
package main
import "fmt"
type Symbol uint8
const (
Cross Symbol = iota
Circle
Dot
)
type GameStatus uint8
const (
GameInProgress GameStatus = iota
GameDraw
FirstPlayerWin
SecondPlayerWin
)
type iPlayer interface {
getSymbol() Symbol
getNextMove() (int, int, error)
getID() int
}
var (
MovesPlayer1 = [4][2]int{{1, 1}, {2, 0}, {2, 2}, {2, 1}}
MovesPlayer2 = [4][2]int{{1, 2}, {0, 2}, {0, 0}, {0, 0}}
)
type humanPlayer struct {
symbol Symbol
index int
id int
}
func (h *humanPlayer) getSymbol() Symbol {
return h.symbol
}
func (h *humanPlayer) getNextMove() (int, int, error) {
if h.symbol == Cross {
h.index = h.index + 1
return MovesPlayer1[h.index-1][0], MovesPlayer1[h.index-1][1], nil
} else if h.symbol == Circle {
h.index = h.index + 1
return MovesPlayer2[h.index-1][0], MovesPlayer2[h.index-1][1], nil
}
return 0, 0, fmt.Errorf("Invalid Symbol")
}
func (h *humanPlayer) getID() int {
return h.id
}
type computerPlayer struct {
symbol Symbol
id int
}
func (c *computerPlayer) getSymbol() Symbol {
return c.symbol
}
func (c *computerPlayer) getNextMove() (int, int, error) {
//To be implemented
return 0, 0, nil
}
func (c *computerPlayer) getID() int {
return c.id
}
type board struct {
square [][]Symbol
dimension int
}
func (b *board) printBoard() {
for i := 0; i < b.dimension; i++ {
for j := 0; j < b.dimension; j++ {
if b.square[i][j] == Dot {
fmt.Print(".")
} else if b.square[i][j] == Cross {
fmt.Print("*")
} else {
fmt.Print("o")
}
}
fmt.Println("")
}
}
func (b *board) markSymbol(i, j int, symbol Symbol) (bool, Symbol, error) {
if i > b.dimension || j > b.dimension {
return false, Dot, fmt.Errorf("index input is greater than dimension")
}
if b.square[i][j] != Dot {
return false, Dot, fmt.Errorf("input square already marked")
}
if symbol != Cross && symbol != Circle {
return false, Dot, fmt.Errorf("incorrect Symbol")
}
b.square[i][j] = symbol
win := b.checkWin(i, j, symbol)
return win, symbol, nil
}
func (b *board) checkWin(i, j int, symbol Symbol) bool {
//Check Row
rowMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[i][k] != symbol {
rowMatch = false
}
}
if rowMatch {
return rowMatch
}
//Check Row
columnMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[k][j] != symbol {
columnMatch = false
}
}
if columnMatch {
return columnMatch
}
//Check diagonal
diagonalMatch := false
if i == j {
diagonalMatch = true
for k := 0; k < b.dimension; k++ {
if b.square[k][k] != symbol {
diagonalMatch = false
}
}
}
return diagonalMatch
}
type game struct {
board *board
firstPlayer iPlayer
secondPlayer iPlayer
firstPlayerTurn bool
moveIndex int
gameStatus GameStatus
}
func initGame(b *board, p1, p2 iPlayer) *game {
game := &game{
board: b,
firstPlayer: p1,
secondPlayer: p2,
firstPlayerTurn: true,
gameStatus: GameInProgress,
}
return game
}
func (g *game) play() error {
var win bool
var symbol Symbol
for {
if g.firstPlayerTurn {
x, y, err := g.firstPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.firstPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = false
g.printMove(g.firstPlayer, x, y)
} else {
x, y, err := g.secondPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.secondPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = true
g.printMove(g.secondPlayer, x, y)
}
g.moveIndex = g.moveIndex + 1
g.setGameStatus(win, symbol)
if g.gameStatus != GameInProgress {
break
}
}
return nil
}
func (g *game) setGameStatus(win bool, symbol Symbol) {
if win {
if g.firstPlayer.getSymbol() == symbol {
g.gameStatus = FirstPlayerWin
return
} else if g.secondPlayer.getSymbol() == symbol {
g.gameStatus = SecondPlayerWin
return
}
}
if g.moveIndex == g.board.dimension*g.board.dimension {
g.gameStatus = GameDraw
return
}
g.gameStatus = GameInProgress
}
func (g *game) printMove(player iPlayer, x, y int) {
symbolString := ""
symbol := player.getSymbol()
if symbol == Cross {
symbolString = "*"
} else if symbol == Circle {
symbolString = "o"
}
fmt.Printf("Player %d Move with Symbol %s at Position X:%d Y:%d\n", player.getID(), symbolString, x, y)
g.board.printBoard()
fmt.Println("")
}
func (g *game) printResult() {
switch g.gameStatus {
case GameInProgress:
fmt.Println("Game in Between")
case GameDraw:
fmt.Println("Game Drawn")
case FirstPlayerWin:
fmt.Println("First Player Win")
case SecondPlayerWin:
fmt.Println("Second Player Win")
default:
fmt.Println("Invalid Game Status")
}
g.board.printBoard()
}
func main() {
board := &board{
square: [][]Symbol{{Dot, Dot, Dot}, {Dot, Dot, Dot}, {Dot, Dot, Dot}},
dimension: 3,
}
player1 := &humanPlayer{
symbol: Cross,
id: 1,
}
player2 := &humanPlayer{
symbol: Circle,
id: 2,
}
game := initGame(board, player1, player2)
game.play()
game.printResult()
}
输出
在上述程序中,我们同样在humanPlayer 类中固定了两个玩家的步伐。基于这些步伐,以下是输出结果。
Player 1 Move with Symbol * at Position X:1 Y:1
...
.*.
...
Player 2 Move with Symbol o at Position X:1 Y:2
...
.*o
...
Player 1 Move with Symbol * at Position X:2 Y:0
...
.*o
*..
Player 2 Move with Symbol o at Position X:0 Y:2
..o
.*o
*..
Player 1 Move with Symbol * at Position X:2 Y:2
..o
.*o
*.*
Player 2 Move with Symbol o at Position X:0 Y:0
o.o
.*o
*.*
Player 1 Move with Symbol * at Position X:2 Y:1
o.o
.*o
***
First Player Win
o.o
.*o
***
结论
这就是关于设计井字棋游戏的内容。希望你喜欢这个教程。请在评论中提供你的反馈。
二进制加法程序
概述
目标是将两个给定的二进制数相加。二进制数仅由数字 0 和 1 组成。以下是每个数字的二进制加法逻辑
-
0+0 = 和为 0,进位为 0
-
0+1 = 和为 1,进位为 0
-
1+0 = 和为 0,进位为 0
-
1+1 = 和为 0,进位为 1
-
1+1+1 = 和为 1,进位为 1
示例
Input: "101" + "11"
Output: "1000"
Input: "111" + "101"
Output: "1100"
程序
以下是相应的程序
package main
import (
"fmt"
"strconv"
)
func addBinary(a string, b string) string {
lenA := len(a)
lenB := len(b)
i := lenA - 1
j := lenB - 1
var output string
var sum int
carry := 0
for i >= 0 && j >= 0 {
first := int(a[i] - '0')
second := int(b[j] - '0')
sum, carry = binarySum(first, second, carry)
output = strconv.Itoa(sum) + output
i = i - 1
j = j - 1
}
for i >= 0 {
first := int(a[i] - '0')
sum, carry = binarySum(first, 0, carry)
output = strconv.Itoa(sum) + output
i = i - 1
}
for j >= 0 {
second := int(b[j] - '0')
sum, carry = binarySum(0, second, carry)
output = strconv.Itoa(sum) + output
j = j - 1
}
if carry > 0 {
output = strconv.Itoa(1) + output
}
return output
}
func binarySum(a, b, carry int) (int, int) {
output := a + b + carry
if output == 0 {
return 0, 0
}
if output == 1 {
return 1, 0
}
if output == 2 {
return 0, 1
}
if output == 3 {
return 1, 1
}
return 0, 0
}
func main() {
output := addBinary("101", "11")
fmt.Println(output)
output = addBinary("111", "101")
fmt.Println(output)
}
输出
1000
1100
求二叉树高度的程序
概述
目标是获取二叉树的高度。例如,假设我们有如下二叉树

那么这个二叉树的高度是 3。
程序
以下是相应的程序
package main
import (
"fmt"
)
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
if root.Left == nil && root.Right == nil {
return 1
}
lHeight := maxDepth(root.Left)
rHeight := maxDepth(root.Right)
if lHeight >= rHeight {
return lHeight + 1
} else {
return rHeight + 1
}
}
func main() {
root := TreeNode{Val: 1}
root.Left = &TreeNode{Val: 2}
root.Left.Left = &TreeNode{Val: 4}
root.Right = &TreeNode{Val: 3}
root.Right.Left = &TreeNode{Val: 5}
root.Right.Right = &TreeNode{Val: 6}
height := maxDepth(&root)
fmt.Println(height)
}
输出
3
检查给定的树是否是二叉搜索树
概述
我们可以使用以下策略来判断给定的树是否是二叉搜索树(BST)。
-
对于给定的当前节点,如果左子树和右子树都是二叉搜索树(BST)
-
左子树中的最大值小于当前节点值
-
右子树中的最小值大于当前节点值
下面是相应的程序。
程序
package main
import (
"fmt"
"math"
)
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func isValidBST(root *TreeNode) bool {
if root == nil {
return true
}
isValid, _, _ := isValidBSTUtil(root)
return isValid
}
func isValidBSTUtil(node *TreeNode) (bool, int, int) {
if node.Left == nil && node.Right == nil {
return true, node.Val, node.Val
}
min := node.Val
max := node.Val
isValidLeft := true
var leftMin, leftMax int
if node.Left != nil {
isValidLeft, leftMin, leftMax = isValidBSTUtil(node.Left)
if !isValidLeft {
return false, 0, 0
}
if node.Val <= leftMax {
return false, 0, 0
}
min = leftMin
}
isValidRight := true
var rightMin, rightMax int
if node.Right != nil {
isValidRight, rightMin, rightMax = isValidBSTUtil(node.Right)
if !isValidRight {
return false, 0, 0
}
if node.Val >= rightMin {
return false, 0, 0
}
max = rightMax
}
return true, min, max
}
func minOfFour(a, b, c, d int) int {
return int(math.Min(float64(a), math.Min(float64(b), math.Min(float64(c), float64(d)))))
}
func maxOfFour(a, b, c, d int) int {
return int(math.Max(float64(a), math.Max(float64(b), math.Max(float64(c), float64(d)))))
}
func main() {
root := TreeNode{Val: 2}
root.Left = &TreeNode{Val: 1}
root.Right = &TreeNode{Val: 3}
isValidBST := isValidBST(&root)
fmt.Println(isValidBST)
}
输出
true
Whatsapp 系统设计
目录
-
概述
-
Whatsapp 无法通过 HTTP 协议工作
-
在 Whatsapp 中,一致性比可用性更重要
-
一对一消息
-
高级设计
-
让我们看看需要哪些 API
-
让我们看看每个功能是如何工作的
-
已读回执将如何工作
-
如果用户 B 处于离线状态怎么办
-
如何确保在用户 B 端消息的顺序
-
如果连接用户的机器之一发生故障怎么办
-
竞态条件怎么办
-
如果用户 A 处于离线状态怎么办
-
-
在线状态和最后一次见面将如何工作
-
群组消息
-
上传图片或视频
-
其他常见组件
-
非功能性需求
-
可扩展性
-
低延迟
-
警报和监控
-
向用户位置靠近
-
避免单点故障
-
-
结论
概述
在回答任何系统设计问题时,重要的是要记住系统设计问题可能非常宽泛。因此,切勿直接跳到解决方案。与面试官讨论使用场景是非常有益的,这样可以理解他在寻找什么。决定你将包括在系统设计中的一组功能。
这也是面试官所关注的方面之一。他们可能在寻找
-
你是如何进行需求分析的
-
你是否能够列出所有需求
-
你在问什么问题。
同时,在进行系统设计时,逐步进行设计非常重要。你需要先列出系统设计将支持的所有功能。首先讨论第一个功能的设计,然后再扩展其他功能的设计。
Whatsapp 无法通过 HTTP 协议工作。
设计 WhatsApp 时需要记住的一点是,它为每个用户保持连接。因此,需要注意的要点是,每个用户都会保持一个持久的连接,这个持久连接与服务器之间保持。
如果你考虑 WhatsApp,显然它不能通过 HTTP 协议工作。
这是因为 HTTP 是一个客户端到服务器的协议。它是一种请求-响应架构,客户端发送请求,服务器发送响应。由于它是客户端到服务器的协议,因此服务器无法与客户端通信。此外,持久连接也没有保持。
因此,Whatsapp 需要通过 TCP 协议进行通信,这是点对点通信。以下是一些选项。
-
HTTP 长轮询 – 在这种方式中,客户端在向服务器发送请求后会等待一段时间。涉及长轮询。然后,客户端可以在特定时间后重新发起请求。
-
Web sockets – 这是一种完全双向连接,客户端可以与服务器通信,服务器也可以与客户端通信。在这种方式下,服务器或客户端可以随时发送/接收数据,连接始终保持开放。
总的来说,Web sockets 最适合我们的场景。Web sockets 的另一个优点是它们具有粘性会话,如果特定用户需要连接到特定服务器并连接到服务器端的某个实例,那么它将始终与该实例保持连接。该用户的所有请求都将只发送到那个特定实例。因此,这使得 Web sockets 成为点对点通信的一个很好的选择。
在进行任何服务的系统设计时,另一个需要考虑的点是不要立即跳到解决方案上。因为应用程序可能具有很多功能,最好先明确你将要在系统设计中包含哪些功能。这一点非常重要,必须进行讨论。另一个需要记住的重要事项是要考虑非功能性需求。一些非功能性需求可能包括:
-
单点故障
-
可扩展性 – 扩展到数百万的用户
-
可用性
-
低延迟或性能
-
容错性
-
存储估算
-
成本估算
我们将在系统设计中针对以下功能进行设计。
-
一对一消息
-
群组消息
-
已读回执 / 在线状态
-
消息存储
-
图片/视频消息
在 WhatsApp 中,一致性比可用性更为重要。
对于 Whatsapp 来说,可用性是一个重要因素,但一致性更为重要。基本上,比可用性更重要的是确保消息能够按顺序传送并被所有用户接收。
一对一消息传递
对于一对一消息传递,让我们来看一下所需的高层次设计。
高层设计
从高层次来讨论,我们来看看整体流程是什么样的,存在哪些服务。
-
将会有一个API 网关,所有用户的请求都会通过它。
-
将会有一个会话服务。会话服务将包含一组实例,用户将通过 WebSocket 连接到这些实例。由于每台机器上可打开的 WebSocket 数量有限,具体数量取决于负载。因此,根据用户数量,我们可以配置相应数量的机器。
-
这个会话服务将连接到一个分布式 Redis集群,该集群将包含关于哪个用户连接到哪个机器的信息。这些信息是暂时的,直到用户断开连接,因此我们可以使用分布式 Redis来存储。会话服务将负责维护用户 ID 和机器 ID 的映射关系。这个服务将会
-
如果有用户连接到某台机器,将会插入数据。
-
当用户断开连接时。
-
-
除此之外,会话服务将是一个简单的服务,意味着它只会接受连接,并将任何请求转发到一个SNS/Kafka主题。
-
会话服务将在接收到任何用户活动时,发布消息到一个 SNS 主题或 Kafka。
-
将会有一个用户活动服务,它是一个工作服务,监听这个 SNS/Kafka 主题。接收到消息后,它将与分布式 Redis 交互,识别接收方连接的用户。这项服务还将处理用户离线的情况。一旦获取所有信息,它将把消息发送到另一个服务,那个服务也是一个工作服务。这个服务将是消息外发服务。
-
将会有一个消息外发服务,这是一个工作服务,它的工作是将外发消息发送回用户。此服务不包含任何业务逻辑。它只会接受一个包含发送消息的详细信息的消息,这些信息包括消息内容、接收人以及用户连接的机器。这是一个非常简单的服务,完全不包含任何业务逻辑。
-
我们需要一个数据库来存储消息,如果接收方用户离线。我们可以使用Mongo DB来实现。
让我们看看需要哪些 API
-
发送文本消息
-
获取所有未读消息(当用户在离线一段时间后重新上线时)。
Mongo DB将有一个表来存储消息。我们可以把这个表命名为消息表。
消息表
消息表中将包含以下字段。
-
消息 ID
-
发送方用户 ID
-
接收方用户 ID
-
类型 – 可能是文本、图片或视频
-
body – 实际的消息内容。
-
created
-
updated
-
is_received
-
is_group – 这条消息是否为群组消息。
-
group_id – 仅在消息为群组消息时设置。
让我们看看每个功能如何工作
用户 A 将通过 WebSocket 连接到会话服务的某一台机器。假设它连接到机器 2。会在分布式 Redis 中创建一条用户 A 到机器 1 的映射。同样,假设用户 B 连接到机器 3,也会在 Redis 中创建用户 B 到机器 3 的映射。
-
用户 A 将向用户 B 发送一条消息。消息会传送到机器 1 上的会话服务,然后被发送到 Kafka/SNS 主题。
-
用户活动服务/工作者将监听这条消息。
-
它将检查分布式 Redis 以确认用户 B 连接到哪台机器。然后,它会再次向不同的 Kafka/SNS 主题发布两条消息。一条消息是发送给用户 B 的交付消息,另一条消息是向用户 A 确认消息已经发送。这些消息将被 消息出站服务 监听。
-
消息出站服务 将获取这两条消息并处理它们。这个服务是一个傻瓜服务,只知道将消息转发到正确的机器,从而确保消息能够送达用户。它只与 会话服务 通信,其他的什么也不做。
下面是相应的架构图。该图表示了用户 A 向用户 B 发送消息的流程。

让我们来理解上述图示。
用户 A 的流程
-
用户 A 调用 API 网关。
-
用户身份验证通过令牌服务进行。
-
用户 A 连接到第 1 号机器。
-
会在 Redis 中创建用户 ID 到机器 ID 的映射。
-
发送给用户 B 的消息会发布到该主题上。
-
它被用户活动服务/工作者提取。
-
它将从 Redis 中查询,了解用户 B 当前连接的机器。如果用户 B 离线,MongoDB 将发挥作用。我们稍后会讨论这个问题。
-
在确认用户 B 连接的机器后,它会将消息发布到用于出站消息服务/工作者的主题。
-
出站消息服务/工作者从主题中获取这条消息。该消息包含要发送的消息内容、接收方和用户当前连接的机器。这个服务非常简单,根本没有任何业务逻辑。
-
它会在与用户 B 连接的机器 3 上发起 API 调用。
-
消息通过 WebSocket 发送到用户 B。
用户 B 的流程
-
用户 B 调用 API 网关。
-
用户身份验证通过令牌服务进行。
-
用户 B 连接到第 3 号机器。
还有一些未解答的问题需要解决。
-
阅读回执将如何工作。
-
如果用户 B 离线怎么办?
-
如何确保在用户 B 端消息的顺序。这意味着,如果用户 A 发送了两条消息 M1 和 M2,且顺序为 M1 后 M2,那么用户 B 也应该按相同的顺序接收这些消息,即用户 B 应该先看到 M1,然后是 M2。
-
如果用户连接的某台机器发生故障怎么办
-
那么竞争条件(race condition)该如何处理呢?我们也将讨论一个竞争条件的例子。
-
如果用户 A 离线怎么办
让我们逐一讨论这些点
如何处理已读回执
一旦用户 B 接收到消息,确认消息将再次发送给机器 3,然后到用户活动服务,再到消息外发服务,最后发送回机器 1,传递给用户 A。当用户 B 读取消息时,也会发生相同的流程。这就是已读回执的工作方式。
如果用户 B 离线怎么办
如果用户离线,WhatsApp 会将消息存储最长达 30 天。因此,在这种情况下,WhatsApp 会将消息存储在其数据库中,数据库是 Mongo DB。所以当用户上线并且首次建立连接时,以下是流程。
-
假设用户 B 将连接到机器 7。系统会在分布式 Redis 中创建一条记录。然后,它会在一个主题上发布消息。
-
用户活动服务将获取这条消息。接着,它会检查数据库中是否存在用户 B 的所有消息。
-
系统会将这些消息发送给用户外发服务,然后通过机器 7 发送给用户 B。
-
一旦用户 B 收到所有这些消息,系统将向所有发送者发送确认消息,表示该消息已被接收。我们上面讨论的相同流程将会继续。
-
一旦用户 B 读取了所有这些消息,系统将向所有发送者发送确认消息,表示该消息已被读取。在发送完确认消息后,消息将从数据库中删除,可能通过另一个服务,即 清理服务。
如何确保在用户 B 端消息的顺序
在服务器端,每条消息的处理是无状态的,这意味着每条消息都独立处理,不受其他消息的影响。以下是一种可以用来确保消息顺序的方法。
-
每条消息都会有一个父消息 ID。父消息 ID 是指在消息顺序中,当前消息前一条消息的 ID。WA 客户端将使用 UUID 来生成这个消息 ID。
-
每条消息都会被发送到 WA 客户端。由 WA 客户端负责展示有序的消息。例如,假设有三条消息。
-
M1 的父消息 ID 为 null,假设这是第一次发送的消息。
-
M2 的父消息 ID 是 M1 的
-
M3 的父消息 ID 是 M2 的
-
-
这三条消息将以无状态的方式逐个处理,并返回到 WA 客户端。如果 WA 客户端收到某条消息,但没有收到当前消息的父消息 ID 对应的消息,它会等待该消息到达,而不是直接显示。例如,假设 WA 客户端收到 M1 和 M3 消息,但没有收到 M2 消息。那么它只会向用户显示 M1,并等待 M2 到达。它怎么知道需要等待 M2 呢?因为 M3 的父消息 ID 是 M2 的,它知道有一条消息丢失了。
-
如果 M2 消息没有在规定时间内到达怎么办?在这种情况下,用户 B 的 WA 客户端可以要求用户 A 重新发送这条消息。
这是确保消息顺序的一种思路。
如果用户连接的某台机器宕机怎么办
假设某个实例终止了,而用户恰好连接到这个实例。可能是因为自动扩缩容,也可能是因为维护活动。在这种情况下,用户将重新建立连接,并且可能会连接到另一台机器。一旦连接建立,Redis 条目将会更新,反映新的机器 ID。
可能会有几条本应发送给该用户的消息在传输过程中失败。但由于每个地方都有重试机制,并且会带有一些延迟和抖动,因此当用户上线时,所有的消息都会通过新的连接正确发送。
那如果出现竞态条件怎么办
例如,用户 A 向用户 B 发送了一条消息。当时用户 B 处于离线状态,消息被保存在数据库中。但在消息保存之前,用户 B 上线并从数据库中获取了所有待处理消息。但它无法获取到用户 A 的最新消息。
为了防止这种情况,客户端可以定期从数据库中获取所有处于未送达状态的消息。
如果用户 A 处于离线状态怎么办
如果用户 A 处于离线状态,那么 Whatsapp 客户端会在用户 A 上线之前将消息存储起来。
在线状态和最后一次见面如何运作
为此,新的服务端将维护一个额外的表格,我们可以称之为user_last_seen服务端。该表格中的字段如下:
-
user_id
-
last_seen
我们来看一下这个表格如何更新。假设有两个用户 A 和 B。
对于用户 A
-
这个表格会在用户 A 进行任何用户活动时更新,任何由用户发起的活动都会被视为用户活动。如果用户进行活动,系统会将信息发送到会话服务,并在 SNS/Kafka 主题上发布一条消息。user_last_seen服务端也会通过队列订阅这个主题。对于每个用户活动,它会更新表格中的最后一次见面字段。
-
也可能出现用户刚在线但没有进行任何活动的情况。在这种情况下,会发送一个心跳消息,并更新该表格。
-
请注意,这张表格不会因非用户活动而更新。例如,当用户的 WhatsApp 未打开并正在获取消息时,这就是一种非用户活动。
对于用户 B
-
用户 B 想要获取用户 A 的在线状态。它会发送请求。请求会到达 user_last_seen 服务。
-
我们可以设置一个阈值。如果用户的 last_seen 字段距离现在不到 2 秒,它将把状态发送为在线,用户 B 将看到用户 A 在线。
-
如果最后一次查看字段距离现在超过 2 秒,则会将状态发送为离线(false),并返回最后查看的时间戳。这个时间戳将显示给用户 B。
群组消息
让我们来看看群组消息是如何工作的。我们可以使用一个群组服务,它将作为一个工作节点。它将有以下数据库。
群组表
它将包含以下字段
-
group_id
-
group_title
-
创建
-
更新
-
group_image_id
GroupId-UserId 映射
它将包含以下字段
-
group_id
-
user_id
-
is_admin
一个重要的要点是,这个 GroupId-UserId 会根据 groupId 分片,这样当获取属于特定 groupId 的所有用户 ID 时,调用只会发送到某个分片。
我们将提供以下 API
-
群组创建
-
群组成员添加
-
群组成员删除
-
群组成员管理员
-
群组成员移除
-
群组标题更新
-
群组图片更新
-
群组消息发送
让我们来看看群组消息是如何发送的。
-
一个群组有四个用户:A、B、C 和 D
-
用户 A 想要向整个群组发送消息
-
它调用发送群组消息的功能,并同时发送群组 ID 和消息。消息到达用户所连接的机器,并由机器将消息发布到一个主题。
-
它由群组服务选择。该服务从群组表中获取所有群组成员。对于每个群组成员,它将消息重新分发到不同的主题。
-
所有这些消息再次被群组服务或工作节点选择。每条消息都被处理并发送给每个群组成员。
-
当任何群组成员接收到消息时,会向消息发送者发送一条确认回执。
-
如果任何群组成员阅读了消息,则会再次向消息发送者发送确认回执。
请注意,如果任何群组成员处于离线状态,则会在 MongoDB 的消息表中为该成员创建一个条目。因此,我们在消息表中有 group_id 和 is_group 字段。
以下是相应的架构图。该图表示从用户 A 到用户 B 和用户 C 发送消息的流程,他们属于同一个 WhatsApp 群组。

让我们理解上面的图示。
用户 A 的流程
-
用户 A 向 API 网关发起调用。
-
用户认证通过 Token 服务进行
-
用户 A 连接到编号为 1 的盒子
-
会在 Redis 中为 userId-machineID 映射创建一条条目。
-
发送给群组的消息会发布到该主题。
-
它由小组服务/工作者接收
-
它从 Mongo 数据库中获取小组的所有其他成员
-
对于每个成员,它会检查 Redis,以了解每个成员的连接情况。如果任何成员离线,则会通过 Mongo 数据库获取信息。
-
它将消息分发给小组中每个其他成员。9.1 – 它将消息发送给用户 B。9.2 – 它将消息发送给用户 C
-
两条消息都由外发消息服务/工作者接收。消息包含了发送的消息内容、发送对象以及用户连接的机器信息。
-
它会在连接到用户 B 的机器 3 上进行 API 调用(11.1),并在连接到用户 C 的机器 4 上进行 API 调用(11.2)
-
消息随后通过 WebSockets 发送给用户 B 和用户 C。
用户 B 的流程
-
用户 B 发起对 API 网关的调用
-
用户认证通过令牌服务进行。
-
用户 B 已连接到编号为 3 的盒子
用户 C 的流程
-
用户 C 发起对 API 网关的调用
-
用户认证通过令牌服务进行
-
用户 C 已连接到编号为 4 的盒子
上传图片或视频
让我们来看一下图片和视频上传的工作流程。对于图片和视频上传,我们可以假设上传的不会是图片或视频的原始大小。客户端端会创建其低分辨率版本,然后进行上传。即使是任何图片和视频的低分辨率版本也只有几 KB。它们可以直接上传到存储提供商。例如,假设存储提供商是 AWS S3,那么以下是流程:
-
假设用户 A 在其 WA 客户端上希望发送一个请求,要求上传一张图片。客户端将向服务器发送请求,请求获取预签名的 URL,客户端可以通过该 URL 上传图片。
-
服务器将响应一个预签名的 URL,其有效期可能为几小时。您可以阅读这篇文章了解预签名 URL 的概念:
docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html。基本上,这是一个已经通过令牌签名的 URL,因此可以直接上传到该 URL,无需进一步认证。这也被称为直接上传。服务器还会返回图像 ID。 -
客户端将图片上传到该 URL。图片将直接存储在 S3 中。
-
现在客户端将请求向接收者发送图片/视频上传消息。请求中还将传递图像 ID。
-
服务器将通过我们上面讨论的单对单消息流程,将消息发送给用户 B。
假设有多个用户同时上传相同的图片,这种情况可能会发生,例如某个特定的梗图变得流行。它将被多个用户发送。如果每次发送时都存储该图片的每个实例,将会浪费存储空间。这里有优化的空间。
我们可以做的是在客户端计算图像的摘要或哈希值。这个哈希值或摘要将被发送到服务器,服务器将检查该摘要或哈希值是否已存在。如果存在,服务器将直接返回该图像的 ID,而不返回预签名 URL。这样客户端就会知道图像已经存在,而不会再次上传图像。它只需使用图像 ID 来发送消息。
让我们看看这个方案的图示。正如图示所示,用户 A 和用户 B 使用直接上传和直接下载与 S3 或其他存储层进行数据传输,不通过 API 网关,从而避免了图像/视频传输过程中大量数据的高成本。

其他常见组件
其他常见组件可能包括:
-
用户服务 – 存储用户个人资料信息。
-
Token/认证服务 – 用户令牌管理
-
短信服务 – 用于向用户发送任何类型的消息。例如:一次性密码(OTP)
-
分析服务 – 用于跟踪任何类型的分析数据。
非功能性需求
我们已经讨论了所有功能需求的系统设计。现在,让我们讨论一些非功能性需求。
-
可扩展性
-
可用性
-
低延迟
-
向用户位置靠近。
-
避免单点故障。
可扩展性
上述设计中需要考虑的第一点是可扩展性因素。系统中每个组件的可扩展性非常重要。以下是可能遇到的可扩展性挑战及其可能的解决方案:
-
会话服务中的每台机器只能承载有限数量的连接。因此,根据当前在线用户数量,可以设置机器和实例的数量。例如,一台机器有大约 65000 个端口。
-
你的 Kafka 系统可能无法承受如此大的负载。我们可以进行水平扩展,但有其限制。如果这成为瓶颈,那么根据地理位置或用户 ID,我们可以拥有两个或更多这样的系统。可以使用服务发现来确定请求需要访问哪个 Kafka 系统。
-
对其他服务也可以采用类似的方式。
-
可扩展性的另一个重要因素是,我们已经设计了系统,使得没有任何服务被过多的任务所压垮。服务之间有明确的职责分离,且我们在服务职责过重时进行了拆分。
低延迟
消息可以从客户端以批次的形式发送,这样可能减少往返时间。可用性为了使系统具有高度可用性,几乎所有组件都需要具备冗余/备份。以下是一些需要完成的事项。
-
对于我们的数据库,我们需要启用复制。每个主分片节点应该有多个从节点。
-
对于分布式 Redis 集群,我们还需要复制。
-
为了实现数据冗余,我们也可以采取多区域架构。如果其中一个区域出现故障,这可能是其中一个好处。
-
还可以设置灾难恢复
警报和监控
警报和监控也是非常重要的非功能性需求。我们应该监控每一个服务,并设置适当的警报。可以监控的一些内容包括:
-
API 响应时间
-
内存消耗
-
CPU 消耗
-
磁盘空间消耗
-
队列长度
-
….
靠近用户位置
这里有几种架构可以选择。其中一种是单元架构(Cell Architecture)。你可以在这里了解更多关于单元架构的内容 – github.com/wso2/reference-architecture/blob/master/reference-architecture-cell-based.md
避免单点故障
单点故障是指系统中的某个部分,如果停止工作,可能导致整个系统崩溃。我们应该在设计中尽量避免任何单点故障。通过冗余和多区域部署,我们可以防止此类问题。
结论
以上就是关于 WhatsApp 系统设计的内容。希望你喜欢这篇文章。请在评论中分享反馈。
字符串交错程序
概述
给定三个字符串s1、s2、s3。判断字符串s3是否是字符串的交错组合。
如果满足以下条件,s3将是字符串s1和s2的交错字符串。
- s3包含s1和s2的所有字符,并且每个字符串中的字符顺序保持不变。
示例
s1: aabcc
s2: dbbca
s3: aadbbcbcac
Output: true
递归解法
以下是相同问题的递归解法
package main
import "fmt"
func main() {
output := isInterleave("aabcc", "dbbca", "aadbbcbcac")
fmt.Println(output)
output = isInterleave("", "", "")
fmt.Println(output)
}
func isInterleave(s1 string, s2 string, s3 string) bool {
s1Rune := []rune(s1)
s2Rune := []rune(s2)
s3Rune := []rune(s3)
lenS1 := len(s1Rune)
lenS2 := len(s2Rune)
lenS3 := len(s3Rune)
if lenS1+lenS2 != lenS3 {
return false
}
return isInterleaveUtil(s1Rune, s2Rune, s3Rune, 0, 0, 0, lenS1, lenS2, lenS3)
}
func isInterleaveUtil(s1, s2, s3 []rune, x, y, z, lenS1, lenS2, lenS3 int) bool {
if x == lenS1 && y == lenS2 && z == lenS3 {
return true
}
if x < lenS1 && z < lenS3 && s1[x] == s3[z] {
match := isInterleaveUtil(s1, s2, s3, x+1, y, z+1, lenS1, lenS2, lenS3)
if match {
return true
}
}
if y < lenS2 && z < lenS3 && s2[y] == s3[z] {
return isInterleaveUtil(s1, s2, s3, x, y+1, z+1, lenS1, lenS2, lenS3)
}
return false
}
输出
true
true
如果你注意到上面的程序,许多子问题被反复计算,因此该解法的复杂度是指数级的。我们可以在这里使用动态规划来降低整体时间复杂度。
这是相同问题的程序
动态规划解法
package main
import "fmt"
func main() {
output := isInterleave("aabcc", "dbbca", "aadbbcbcac")
fmt.Println(output)
output = isInterleave("", "", "")
fmt.Println(output)
}
func isInterleave(s1 string, s2 string, s3 string) bool {
s1Rune := []rune(s1)
s2Rune := []rune(s2)
s3Rune := []rune(s3)
lenS1 := len(s1Rune)
lenS2 := len(s2Rune)
lenS3 := len(s3Rune)
if lenS1+lenS2 != lenS3 {
return false
}
interleavingMatrix := make([][]bool, lenS1+1)
for i := range interleavingMatrix {
interleavingMatrix[i] = make([]bool, lenS2+1)
}
i := 1
k := 1
interleavingMatrix[0][0] = true
for i <= lenS1 && k <= lenS3 {
if s1Rune[i-1] == s3Rune[k-1] {
interleavingMatrix[i][0] = true
i++
k++
} else {
break
}
}
i = 1
k = 1
for i <= lenS2 && k <= lenS3 {
if s2Rune[i-1] == s3Rune[k-1] {
interleavingMatrix[0][i] = true
i++
k++
} else {
break
}
}
for i := 1; i <= lenS1; i++ {
for j := 1; j <= lenS2; j++ {
if s1Rune[i-1] == s3Rune[i+j-1] {
interleavingMatrix[i][j] = interleavingMatrix[i-1][j]
}
if s2Rune[j-1] == s3Rune[i+j-1] && !interleavingMatrix[i][j] {
interleavingMatrix[i][j] = interleavingMatrix[i][j-1]
}
}
}
return interleavingMatrix[lenS1][lenS2]
}
输出
true
true
在 MAC OS 的终端中生成不带破折号的 UUID
概述
dbus-uuidgen 可以在 MAC 上的终端中生成不带破折号的 UUID
以下是相同操作的示例
示例
/ [root] $ dbus-uuidgen
8cb11c2c78a6f100ce0a6d1e612e6bc8
将字谜分组的程序
概述
给定一个字符串数组,编写一个程序将所有字谜分组。来自维基百科
字谜 是通过重新排列另一个单词或短语的字母形成的单词或短语,通常使用所有原始字母且每个字母恰好使用一次。例如,单词anagram本身可以重新排列成nag a ram,单词binary可以重新排列成brainy,单词adobe可以重新排列成abode。
例如
Input: ["art", "tap", "rat", "pat", "tar","arm"]
Output: [["art", "rat", "tar"], ["tap", "pat"], ["arm"]]
以下是策略。
- 复制原始数组。对每个字符串进行排序,复制数组将如下所示
["art", "apt", "art", "apt", "art", "arm"]
- 创建一个映射来存储输出
var output map[string][]int
- 为上述复制数组构建一个字典树,所有字符串都已排序。在插入每个元素后更新上述映射。对于“art”,映射应如下所示,因为 art 在原始数组中的字谜分别位于 0、2 和 5 的位置。
map["art"] = [0,2,4]
- 遍历映射,并通过索引输入字符串数组来打印输出
程序
以下是相应的程序
package main
import (
"fmt"
"sort"
)
func main() {
strs := []string{"art", "tap", "rat", "pat", "tar", "arm"}
output := groupAnagrams(strs)
fmt.Println(output)
strs = []string{""}
output = groupAnagrams(strs)
fmt.Println(output)
strs = []string{"a"}
output = groupAnagrams(strs)
fmt.Println(output)
}
type sortRune []rune
func (s sortRune) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortRune) Less(i, j int) bool {
return s[i] < s[j]
}
func (s sortRune) Len() int {
return len(s)
}
func groupAnagrams(strs []string) [][]string {
anagramMap := make(map[string][]int)
var anagrams [][]string
trie := &trie{root: &trieNode{}}
lenStrs := len(strs)
var strsDup []string
for i := 0; i < lenStrs; i++ {
runeCurrent := []rune(strs[i])
sort.Sort(sortRune(runeCurrent))
strsDup = append(strsDup, string(runeCurrent))
}
for i := 0; i < lenStrs; i++ {
anagramMap = trie.insert(strsDup[i], i, anagramMap)
}
for _, value := range anagramMap {
var combinedTemp []string
for i := 0; i < len(value); i++ {
combinedTemp = append(combinedTemp, strs[value[i]])
}
anagrams = append(anagrams, combinedTemp)
}
return anagrams
}
type trieNode struct {
isWord bool
childrens [26]*trieNode
}
type trie struct {
root *trieNode
}
func (t *trie) insert(input string, wordIndex int, anagramMap map[string][]int) map[string][]int {
inputLen := len(input)
current := t.root
for i := 0; i < inputLen; i++ {
index := input[i] - 'a'
if current.childrens[index] == nil {
current.childrens[index] = &trieNode{}
}
current = current.childrens[index]
}
current.isWord = true
if anagramMap[input] == nil {
anagramMap[input] = []int{wordIndex}
} else {
anagramMap[input] = append(anagramMap[input], wordIndex)
}
return anagramMap
}
输出
[[art rat tar] [tap pat] [arm]]
[[]]
[[a]]
通配符匹配或正则表达式匹配问题
概述
我们给定一个输入正则表达式和输入字符串。正则表达式可以包含两个特殊字符
-
星号‘*’ – 星号匹配零个或多个字符。
-
问号‘?’ – 它匹配任何字符。
目标是判断给定的输入字符串是否匹配正则表达式。
例如
Input String: aa
Regex Sring: aa
Output: true
Input String: ab
Regex Sring: a?
Output: true
Input String: aaaa
Regex Sring: *
Output: true
Input String: aa
Regex Sring: a
Output: false
以下是相同问题的递归解法
递归解法
在递归解法中
-
如果遇到星号,我们有两种情况。我们忽略模式中的字符,继续处理模式中的下一个字符。另一种情况是我们在输入字符串中向前移动一个字符,假设*至少匹配一个字符。基本上,我们需要检查(inputIndex, patternIndex+1)和(inputIndex+1, patternIndex)是否匹配。如果其中一个返回 true,那么输入字符串匹配正则表达式。
-
如果遇到问号?,我们也简单地继续处理(inputIndex+1, patternIndex+1)
-
如果遇到一个简单字符,我们就简单地沿着输入字符串和模式继续前进,也就是说,我们继续处理(inputIndex+1, patternIndex+1)
这是程序
package main
import "fmt"
func main() {
output := isMatch("aa", "aa")
fmt.Println(output)
output = isMatch("aaaa", "*")
fmt.Println(output)
output = isMatch("ab", "a?")
fmt.Println(output)
output = isMatch("adceb", "*a*b")
fmt.Println(output)
output = isMatch("aa", "a")
fmt.Println(output)
output = isMatch("mississippi", "m??*ss*?i*pi")
fmt.Println(output)
output = isMatch("acdcb", "a*c?b")
fmt.Println(output)
}
func isMatch(s string, p string) bool {
runeInputArray := []rune(s)
runePatternArray := []rune(p)
if len(runeInputArray) > 0 && len(runePatternArray) > 0 {
if runePatternArray[len(runePatternArray)-1] != '*' && runePatternArray[len(runePatternArray)-1] != '?' && runeInputArray[len(runeInputArray)-1] != runePatternArray[len(runePatternArray)-1] {
return false
}
}
return isMatchUtil([]rune(s), []rune(p), 0, 0, len([]rune(s)), len([]rune(p)))
}
func isMatchUtil(input, pattern []rune, inputIndex, patternIndex int, inputLength, patternLength int) bool {
if inputIndex == inputLength && patternIndex == patternLength {
return true
} else if patternIndex == patternLength {
return false
} else if inputIndex == inputLength {
if pattern[patternIndex] == '*' && restPatternStar(pattern, patternIndex+1, patternLength) {
return true
} else {
return false
}
}
if pattern[patternIndex] == '*' {
return isMatchUtil(input, pattern, inputIndex, patternIndex+1, inputLength, patternLength) ||
isMatchUtil(input, pattern, inputIndex+1, patternIndex, inputLength, patternLength)
}
if pattern[patternIndex] == '?' {
return isMatchUtil(input, pattern, inputIndex+1, patternIndex+1, inputLength, patternLength)
}
if inputIndex < inputLength {
if input[inputIndex] == pattern[patternIndex] {
return isMatchUtil(input, pattern, inputIndex+1, patternIndex+1, inputLength, patternLength)
} else {
return false
}
}
return false
}
func restPatternStar(pattern []rune, patternIndex int, patternLength int) bool {
for patternIndex < patternLength {
if pattern[patternIndex] != '*' {
return false
}
patternIndex++
}
return true
}
输出
true
true
true
true
false
false
false
动态规划解法
上面的程序并不是一个优化的解法,因为子问题被一遍又一遍地解决。这个问题也可以通过动态规划(DP)来解决。
创建一个名为isMatchingMatrix的二维矩阵,其中
isMatchingMatrix[i][j] 如果输入字符串的前i个字符与模式的前j个字符匹配,则为 true
If both input and pattern is empty
isMatchingMatrix[0][0] = true
If pattern is empty
isMatchingMatrix[i][0] = fasle
If the input string is empty
isMatchingMatrix[0][j] = isMatchingMatrix[0][j - 1] if pattern[j – 1] is '*'
以下是相同问题的程序。
package main
import "fmt"
func main() {
output := isMatch("aa", "aa")
fmt.Println(output)
output = isMatch("aaaa", "*")
fmt.Println(output)
output = isMatch("ab", "a?")
fmt.Println(output)
output = isMatch("adceb", "*a*b")
fmt.Println(output)
output = isMatch("aa", "a")
fmt.Println(output)
output = isMatch("mississippi", "m??*ss*?i*pi")
fmt.Println(output)
output = isMatch("acdcb", "a*c?b")
fmt.Println(output)
}
func isMatch(s string, p string) bool {
runeInput := []rune(s)
runePattern := []rune(p)
lenInput := len(runeInput)
lenPattern := len(runePattern)
isMatchingMatrix := make([][]bool, lenInput+1)
for i := range isMatchingMatrix {
isMatchingMatrix[i] = make([]bool, lenPattern+1)
}
isMatchingMatrix[0][0] = true
for i := 1; i < lenInput; i++ {
isMatchingMatrix[i][0] = false
}
if lenPattern > 0 {
if runePattern[0] == '*' {
isMatchingMatrix[0][1] = true
}
}
for j := 2; j <= lenPattern; j++ {
if runePattern[j-1] == '*' {
isMatchingMatrix[0][j] = isMatchingMatrix[0][j-1]
}
}
for i := 1; i <= lenInput; i++ {
for j := 1; j <= lenPattern; j++ {
if runePattern[j-1] == '*' {
isMatchingMatrix[i][j] = isMatchingMatrix[i-1][j] || isMatchingMatrix[i][j-1]
}
if runePattern[j-1] == '?' || runeInput[i-1] == runePattern[j-1] {
isMatchingMatrix[i][j] = isMatchingMatrix[i-1][j-1]
}
}
}
return isMatchingMatrix[lenInput][lenPattern]
}
输出
true
true
true
true
false
false
false
在 Ruby 语言中打印/输出斜体文本
概述
ANSI 转义码 可用于在控制台中输出粗体文本。请注意
-
在 MAC/Linux 系统终端支持 ANSI 转义码
-
Windows 命令提示符不支持它。在 Windows 上,你可以安装 Cygwin,ANSI 转义码在该环境下可正常工作。
程序
class String
def italic
"\e3m#{self}\e[23m"
end
end
puts "Italic String".italic
输出
![
在 Ruby 语言中打印/输出粗体文本
概述
ANSI 转义码 可以用于在控制台输出粗体文本。请注意
-
MAC/Linux 系统的终端支持 ANSI 转义码
-
Windows 命令提示符不支持 ANSI 转义码。在 Windows 上,你可以安装 Cygwin,ANSI 转义码在该环境下可以正常工作。
程序
class String
def bold
"\e1m#{self}\e[22m"
end
end
puts "Bold String".bold
输出
![
在 Ruby 语言中加载 .env 或环境文件
概述
dotenv gem 可以用来在 Ruby 中加载 .env 或 环境 文件
它接受可变数量的参数,每个参数可以是它需要加载的 .env 文件名
程序
创建一个 local.env 文件,内容如下:
STACK=DEV
DATABASE=SQL
这是程序示例
require 'dotenv'
Dotenv.load("local.env")
puts ENV["STACK"]
puts ENV["DATABASE"]
输出
DEV
SQL
它加载 local.env 文件并给出正确的输出
它还可以用来加载多个 .env 文件。创建一个新的 test.env 文件,内容如下:
TEST=UNIT
require 'dotenv'
Dotenv.load("local.env", "test.env")
puts ENV["STACK"]
puts ENV["DATABASE"]
puts ENV["TEST"]
输出
DEV
SQL
UNIT
这段代码是关于在 Ruby 中加载 .env 文件的。
在 Ruby 语言中打印/输出颜色到控制台
概述
我们可以使用 colorize gem 来实现相同的功能
只需安装 Gem
gem install colorize
程序
require 'colorize'
puts "Printing Blue Color".colorize(:blue)
puts "Printing Red Color".colorize(:red)
puts "Printing Yellow Color".colorize(:yellow)
puts "Printing Gree Color".colorize(:green)
输出

你也可以通过运行下面的代码来检查所有的颜色选项
puts String.colors
它将输出
black
light_black
red
light_red
green
light_green
yellow
light_yellow
blue
light_blue
magenta
light_magenta
cyan
light_cyan
white
light_white
default
使用 Ruby 语言在 RubyMine 中将文件路径转换为终端中的超链接
概述
我们可以在文件路径前添加 file://,使其在 RubyMine 编辑器的终端中变为可点击链接
例如,下面是我们如何创建完整链接的示例
puts "file://" + {filepath}
程序
让我们来看一个相同功能的程序
在 /files 目录下创建一个名为 test.html 的文件,内容为 以下 所示
<html></hmtl>
现在创建一个包含以下内容的 Ruby 文件
main.rb
puts "file://" + "/files/test.html"
运行这个 Ruby 文件
ruby main.rb
输出
file:///files/test.html
这个路径在 RubyMine 中将是可点击的
理解正则表达式中的花括号
概述
花括号在正则表达式中充当重复量词。它们指定了在输入字符串或文本中,前面字符可以出现的次数。它们还可以用来指定一个范围,即指定一个字符出现的最小和最大次数。
它的语法是
{min, max}
在哪里
-
min 表示字符可以出现的最小次数
-
max 表示字符可以出现的最大次数
例如
a{n}
这指定了字符“a”可以出现恰好n次。类似地,对于下面的正则表达式
\d{n}
这指定了任何数字可以出现恰好n次。花括号还可以用来定义一个范围。
例如
-
{m,n} – 至少m次,最多n次
-
{m,} – 至少m次
-
{, n} – 最多n次
让我们在 Ruby 语言中看一个相同的例子
main.ruby
regex = "b{2}"
input = "bb"
match = input.match(/#{regex}/)
puts match
input = "bbb"
match = input.match(/#{regex}/)
puts match
输出
bb
bb
默认情况下,花括号是贪婪的或非懒惰的。这意味着什么?它们会匹配所有可能的字符,并总是更倾向于匹配更多的字符。也可以通过在花括号操作符后加上问号来使花括号操作符变得非贪婪或懒惰。让我们看一个相同的例子。
正如你从输出中看到的那样,在花括号操作符后加上问号操作符后,它会尽可能匹配最少数量的字符,也就是说,它变成了非贪婪模式。
这就是为什么给定正则表达式
ab{2,4}
它会为所有以下输入字符串给出匹配结果abb
abb
abbb
abbbb
对应的 Ruby 语言程序
main.ruby
regex = "ab{2,4}"
input = "abb"
match = input.match(/#{regex}/)
puts match
input = "abbb"
match = input.match(/#{regex}/)
puts match
input = "abbbb"
match = input.match(/#{regex}/)
puts match
输出
abb
abbb
abbbb
而
ab{2,4}? 将始终为所有上述输入字符串匹配abb
对应的程序
regex = "ab{2,4}?"
input = "abb"
match = input.match(/#{regex}/)
puts match
input = "abbb"
match = input.match(/#{regex}/)
puts match
input = "abbbb"
match = input.match(/#{regex}/)
puts match
输出
abb
abb
abb
花括号应用于分组
正则表达式的一部分可以放在一个平衡的圆括号内。这部分现在是一个分组。我们还可以对这个分组应用花括号。花括号会添加到分组之后。
让我们来看一个相同的例子。
regex = "(ab){2}"
input = "ababb"
match = input.match(/#{regex}/)
puts match
input = "ababbc"
match = input.match(/#{regex}/)
puts match
输出
abab
abab
花括号应用于字符类
花括号量词也可以应用于整个字符类。它的含义仍然是一样的。字符类在正则表达式中用方括号表示。让我们看一下相应的程序。
在上面的程序中,我们有以下正则表达式
[ab]{4}
它意味着它将匹配一个长度恰好为 4 且由字符‘a’和‘b’组成的字符串,顺序不限。
这就是为什么正则表达式匹配了以下字符串
abab
aaaa
bbbb
aabb
bbaa
并且它不匹配
aba - String of length 3
abbaa - String of length 5
如何在正则表达式中将花括号作为字面字符使用。
如果需要将花括号作为字面字符使用,可以在开括号或闭括号前放置转义字符。
如果一个右花括号前面没有左花括号,那么它将被视为字面上的右花括号。
这就是正则表达式中关于花括号的所有内容。希望你喜欢这篇文章。如果有任何反馈,请在评论中分享。
在 Ruby 语言中执行系统命令
概述
Ruby 的 system 方法可以用来在 Ruby 中运行系统命令。以下是相应的语法
system(some_command)
关于系统命令的一些重要事项
-
它将等待直到命令执行完毕
-
如果命令成功,它将返回 true
-
如果命令执行失败并返回错误代码,它将返回 false
-
如果命令未找到,它将返回 nil
让我们来看看一个相同的程序
程序
output = system("pwd")
puts "For pwd command: " + output.to_s
output = system("exit 1")
puts "For exit 1 command: " + output.to_s
output = system("pwd1")
puts "For pwd1 command: " + output.to_s
输出
<current_directory>
For pwd command: true
For exit 1 command: false
For pwd1 command:
在上面的程序中,有三种情况
- 当命令成功时
output = system("pwd")
puts "For pwd command: " + output.to_s
在这种情况下,输出是 true
<current_directory>
For pwd command: true
- 当命令失败时
output = system("exit 1")
puts "For exit 1 command: " + output.to_s
在这种情况下,输出是 false
For exit 1 command: false
- 当命令不存在时,例如 pwd1 是一个无效命令
output = system("pwd1")
puts "For pwd1 command: " + output.to_s
在这种情况下,输出是 nil
For pwd1 command:
Ruby 正则表达式:匹配正则表达式中的任何字符
概述
点号‘.’是正则表达式中最常用的元字符之一。它用于匹配任何字符。默认情况下,它不匹配换行符。
现在让我们看看一个简单的程序,展示点号‘.’字符。
程序
match = "a".match(/./)
puts "For a: " + match.to_s
match = "b".match(/./)
puts "For b: " + match.to_s
match = "ab".match(/./)
puts "For ab: " + match.to_s
match = "".match(/./)
puts "For Empty String: " + match.to_s
输出
For a: a
For b: b
For ab: a
For Empty String:
在上面的程序中,我们有一个简单的正则表达式,只包含一个点号字符。
/./
它匹配以下字符和字符串。
a
b
ab
它匹配ab,因为默认情况下,正则表达式不会匹配整个字符串,除非使用锚字符(插入符号和美元符号)。这就是为什么它匹配字符串‘ab’中的第一个字符‘a’并报告匹配。它不会匹配空字符串。
让我们看一个例子,其中正则表达式中有两个点号。
match = "ab".match(/../)
puts "For ab: " + match.to_s
match = "ba".match(/../)
puts "For ba: " + match.to_s
match = "abc".match(/../)
puts "For abc: " + match.to_s
match = "a".match(/../)
puts "For a: " + match.to_s
输出
For ab: ab
For ba: ba
For abc: ab
For a:
在上面的程序中,我们有一个简单的正则表达式,包含两个点号。
/../
它将匹配任何包含至少两个字符的子字符串。
这就是为什么它匹配
ab
ba
abc
并且不会给出匹配(它会匹配一个空字符串)。
a
Ruby 正则表达式或变量中的正则表达式
概述
在 Ruby 中,正则表达式是通过两个斜杠定义的。这是一种区分正则表达式与普通字符串的方式。正则表达式将以这种方式表示
/#{regex}/
因此,也可以将正则表达式赋值给一个变量。下面是相应的示例程序。
程序
regex = "b+"
input = "bb"
match = input.match(/#{regex}/)
puts match
input = "bbb"
match = input.match(/#{regex}/)
puts match
input = "a"
match = input.match(/#{regex}/)
puts match
输出
bb
bbb
注意我们是如何将正则表达式赋值给一个变量的,像这样
regex = "b+"
然后像这样在匹配中使用它
match = input.match(/#{regex}/)
除了正则表达式,你还可以使用其他变量或任何类型的字符串
例如
regex = "b+"
input = "abb"
match = input.match(/a#{regex}/)
puts match
注意这里的正则表达式,我们在前面加上了字符“a”
/a#{regex}/
现在运行上面的程序。它会给出一个匹配结果
abb
在 bash 或终端中,chmod o+w 命令是什么意思
概览
在管理文件权限时,有三个组成部分需要考虑。
-
用户—缩写为‘u’
-
组—缩写为‘g’
-
其他—缩写为‘o’
-
权限(读取/写入/执行),其中读取、写入、执行分别缩写为‘r’、’w’和‘x’
所以o+w意味着授予其他用户写入权限
示例
- 创建文件temp.txt,检查其权限
ls -all | grep temp.txt
-rw-r--r-- 1 root root 0 Aug 9 14:50 temp.txt
请注意,其他用户仅具有读取权限
- 现在运行命令
chmod o+w temp.txt
ls -all | grep temp.txt
-rw-r--rw- 1 root root 0 Aug 9 14:50 temp.txt
请查看上面的输出。执行权限也被授予给其他用户。
在 Ruby 语言中解析文件或字符串的 JSON
概述
ruby 的json模块可以用来解析 ruby 语言中的文件或字符串
ruby-doc.org/stdlib-2.6.3/libdoc/json/rdoc/JSON.html
它提供了一个解析函数,可以用来解析文件。
解析字符串
以下是相应的程序。
require 'json'
parsed = JSON.parse('{"a":"x","b":"y"}')
puts parsed
puts parsed["a"]
puts parsed["b"]
输出
{"a"=>"x", "b"=>"y"}
x
y
解析文件
创建一个名为temp.json的文件,内容如下:
{"a":"x","b":"y"}
现在运行下面的程序
require 'json'
file = File.read('temp.json')
parsed = JSON.parse(file)
puts parsed
puts parsed["a"]
puts parsed["b"]
输出
{"a"=>"x", "b"=>"y"}
x
y
在命令行或终端中运行 Ruby 语言代码 — 单行和多行
概述
可以直接在命令行或终端中运行 Ruby 代码。我们需要传递 -e 标志。以下是相应的语法
ruby -e "<ruby_code>"
程序
下面是一个简单的示例
ruby -e "puts 'hello'"
在终端上运行上述命令,它将输出
hello
如何运行多行 Ruby 代码。下面是相同的示例。你不需要使用 -e 标志。
ruby <<END
puts "Start"
5.times do |i|
puts i
end
puts "End"
END
上述程序将输出
Start
0
1
2
3
4
End
如何在 bash 或终端中处理命令失败
概览
如果命令失败不应停止程序的执行,则可以使用以下方法。
command && echo "Success" || echo "Failure"
-
如果命令失败,则输出将是“Success”。
-
如果上面的命令通过,则输出将是“Failure”
示例
*** 成功案例
在以下示例中,命令是‘pwd’,这是一个有效命令
export command=pwd
$command && echo "Success" || echo "Failure"
输出
{It will print the current directory}
Success
- 失败案例
在以下示例中,命令是‘pwdd’,这是一个无效命令
export command=pwdd
$command && echo "Success" || echo "Failure"
输出
-bash: pwdd: command not found
Failure
它打印Failure,因为pwdd是一个无效命令
检查文件在 Ruby 语言中是否可写
概述
Ruby 的 File 类提供了以下方法,可用于检查当前进程的有效用户和组 ID 是否具有文件的写权限。
这是该函数的签名
writable?(file_name)
如果文件可写,它返回 true,否则返回 false。
程序
在当前目录创建一个名为test.txt的文件。使用相同的用户运行以下程序
writable = File.writable?("test.txt")
puts writable
输出
true
现在使用chown命令更改test.txt文件的用户和组 ID。确保当前用户不属于新的组 ID。
再次运行上述程序。这次它将输出 false。
在 Ruby 语言中获取或提取 URL 的查询参数
概述
URI 模块可以用来解析 URL 并提取所有部分
ruby-doc.org/stdlib-2.5.1/libdoc/uri/rdoc/URI.html
一旦给定的 URL 被正确解析,它将返回 URI 对象。然后我们可以从该 URL 对象中访问查询参数
让我们来看一个实际的程序示例:
我们将解析下面的 URL
https://test:abcd123@techbyexample.com:8000/tutorials/intro?type=advance&compact=false#history
查询参数是 type=advance 和 compact=false,它们在上面的 URL 中通过 & 符号分隔
程序
require 'uri'
uri = "https://test:abcd123@techbyexample.com:8000/tutorials/intro?type=advance&compact=false#history"
pasrse_uri = URI(uri)
puts(pasrse_uri.query)
输出
type=advance&compact=false
它正确地显示了所有查询参数,如输出所示
在 Ruby 语言中从 URL 获取完整的主机名和端口
概述
Ruby 的 URI 模块可用于从 URL 获取所有部分。这个模块可以用于解析 URL 并提取所有部分。ruby-doc.org/stdlib-2.5.1/libdoc/uri/rdoc/URI.html
一旦给定的 URL 被正确解析,它将返回 URI 对象。我们随后可以从 URI 中访问以下信息:
-
协议
-
用户信息
-
主机名
-
端口
-
路径名
-
查询参数
-
片段
一旦我们拥有所有部分,就可以将它们连接起来,以获得完整的主机名和端口。
程序
以下是相同程序的代码:
require 'uri'
uri = "https://test:abcd123@techbyexample.com:8000/tutorials/intro?type=advance&compact=false#history"
pasrse_uri = URI(uri)
hostname = pasrse_uri.scheme + "://" + pasrse_uri.hostname + ":" + pasrse_uri.port.to_s
puts hostname
输出
https://techbyexample.com:8000
将查询参数字符串转换为 Ruby 语言中的查询参数哈希
概述
假设我们有以下查询参数字符串
a=b&x=y
我们希望输出为
{
"a" => "b",
"x" => "y"
}
程序
以下是相同的程序
input_sring = 'a=b&x=y'
query_params_hash = {}
input_string_split = input_sring.split('&')
input_string_split.each do |q|
q_split = q.split('=')
query_params_hash[q_split[0]] = q_split[1]
end
puts query_params_hash
输出
{"a"=>"b", "x"=>"y"}
从 Ruby 语言中的字符串中提取一个 URL
概述
URI 模块可以用来从给定的字符串中提取 URL
ruby-doc.org/stdlib-2.5.1/libdoc/uri/rdoc/URI.html
我们可以使用 URI 模块的 Extract 方法
ruby-doc.org/stdlib-2.5.1/libdoc/uri/rdoc/URI.html#method-c-extract
以下是该方法的签名
URI::extract(str[, schemes][,&blk])
参数
-
str – 这是用于从中提取多个 URL 的输入字符串
-
schemes – 用于限制提取的 URL 范围
程序
首先让我们看看一个提取单个 URL 的程序
require 'uri'
input = "The webiste is https://techbyexample.com:8000/tutorials/intro"
urls = URI.extract(input)
puts(urls)
输出
https://techbyexample.com:8000/tutorials/intro
让我们来看另一个提取多个 URL 的程序
require 'uri'
input = "The webiste is https://techbyexample.com:8000/tutorials/intro amd mail to mailto:contactus@techbyexample.com"
urls = URI.extract(input)
puts(urls)
输出
https://techbyexample.com:8000/tutorials/intro
mailto:contactus@techbyexample.com
如果我们想要限制方案,也可以做到。
require 'uri'
input = "The webiste is https://techbyexample.com:8000/tutorials/intro amd mail to mailto:contactus@techbyexample.com"
urls = URI.extract(input, "https")
puts(urls)
输出
https://techbyexample.com:8000/tutorials/intro
在上面的程序中,我们提供的方案是 https,这就是为什么我们只得到一个输出的原因
在 Ruby 语言中解析 URL 并提取所有部分
概述
URI模块可以用来解析 URL 并提取所有部分 ruby-doc.org/stdlib-2.5.1/libdoc/uri/rdoc/URI.html
一旦给定的 URL 正确解析,它将返回 URI 对象。然后我们可以从 URI 中访问以下信息
-
协议
-
用户信息
-
主机名
-
端口
-
路径名
-
查询参数
-
片段
让我们看看一个相同功能的程序:
我们将解析以下 URL
https://test:abcd123@techbyexample.com:8000/tutorials/intro?type=advance&compact=false#history
然后
-
协议是 HTTPS
-
用户信息 – 用户名是 test,密码是 abcd123\。用户名和密码由冒号:分隔
-
端口是 8000
-
路径是 tutorials/intro
-
查询参数是type=advance和compact=false。它们由&符号分隔
-
片段是history。它会直接跳转到页面中的历史部分。history是指向该页面内特定部分的标识符
程序
require 'uri'
uri = "https://test:abcd123@techbyexample.com:8000/tutorials/intro?type=advance&compact=false#history"
pasrse_uri = URI(uri)
puts(pasrse_uri.scheme)
puts(pasrse_uri.userinfo)
puts(pasrse_uri.host)
puts(pasrse_uri.port)
puts(pasrse_uri.path)
puts(pasrse_uri.query)
puts(pasrse_uri.fragment)
puts(pasrse_uri.to_s)
输出
https
test:abcd123
techbyexample.com
8000
/tutorials/intro
type=advance&compact=false
history
https://test:abcd123@techbyexample.com:8000/tutorials/intro?type=advance&compact=false#history
它正确地输出了所有信息,正如从输出中看到的那样
检查哪个进程在你的 MAC 或 Linux 本地计算机上使用端口 8080
lsof命令可以用来检查哪个进程正在占用端口 8080。lsof命令代表List Of Open File(打开文件列表)。
它提供了一个-i选项,可以用来检查系统中可能已被某些网络连接打开的文件。它列出了所有的 UDP 和 TCP 连接。
输入以下命令
lsof -i tcp:8080
它会告诉你哪个进程正在占用端口 8080。以下是我系统上的输出。
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
main 35631 <user> 3u IPv6 0x5a460c6a5bab1b15 0t0 TCP *:http-alt (LISTEN)
输出中还包含了进程的 PID,我可以用这个 PID 来终止该进程。以下是终止进程的命令:
kill -9 <pid_of_process>
如何使用 SSH 命令在远程服务器上运行命令?
概述
使用带有 -t 参数的 ssh 命令,我们可以在远程服务器上运行命令。
语法:
ssh user@server – t “Command to be executed at server”
示例:
#Running chef-solo on remote server
CHEF_TARGET = "ssh -v ec2-user@ec2-52-24-147-93.us-west-2.compute.amazonaws.com"
CHEF_ROLE="my-app"
ssh ec2-user@ec2-55-124-347-93.us-west-2.compute.amazonaws.com -t "sudo chef-solo --node-name $CHEF_TARGET -o role[$CHEF_ROLE]"
#Performing list command on remote server
ssh ec2-user@10.24.256.11 - t "ls -la"
#Creating a directory at remote server
ssh -i pemfile.pem ec2-user@10.24.256.11 - t "sudo mkdir -p /root/home/test"
注意:用于服务器身份验证时,你可以使用以下两种方式。
-
基于公私钥的身份验证
-
基于密码的身份验证
如何在 Linux 上将命令运行在后台
原文:
techbyexample.com/run-command-in-the-background-on-linux/
概述
-
使用‘&’命令
-
使用‘bg’命令
-
使用‘nohup’工具
使用‘&’命令
在命令末尾加上&以将命令发送到后台。
语法
command &
示例:
jhon-mbp:home jhon$ ping google.com > out 2>&1 &
[1] 17609
jhon-mbp:home jhon$ ping google.com &
[1] 18223
PING google.com (172.217.167.206): 56 data bytes
注意:– 这很危险,因为它会将输出发送到终端,阻止你输入任何命令。因此,如果某个命令输出内容,请将其重定向到文件或其他地方,或者重定向到/dev/null,然后再将命令发送到后台。这就是为什么我将命令的输出重定向到名为‘out’的文件,并加上‘2>&1’将错误也重定向到文件中。
jobs 命令
它会列出所有在后台运行的作业,并在占位符[]中显示作业编号。
jhon-mbp:home jhon$ jobs
[1]+ Running ping google.com > out 2>&1 &
fg 命令
语法:
fg %job_number
注意:- 你可以在运行命令时通过‘&’获取作业编号,也可以在‘jobs’命令的输出中找到它。
示例:
jhon-mbp:home jhon$ fg %1
ping google.com > out 2>&1
^Cjhon-mbp:home jhon$
注意:- 一旦我们将上述命令带到前台,就必须使用‘Ctrl + C’取消它,因为它会阻塞终端。因此,始终保持另一个终端打开,以防某些操作未按计划执行,你可以使用‘kill’或‘pkill’命令从该终端终止该进程。
kill -9 $PID_NUMBER
pkill -9 -f "command name"
使用‘bg’命令
要使用此命令,请按照以下两个步骤进行操作。
-
运行命令后按 Ctrl + Z 停止它
-
现在运行‘bg’命令,它将把上一个命令放到后台执行。
示例:
./git-p4.py clone --p4_spec p4-specs/file-spec.json --repo /root/code/myapp/testrepo -v --revisions @2020/09/09,now
Ctrl + Z #to Stop it
现在运行 bg 命令
bg
使用‘nohup’工具
只需在命令前加上‘nohup’前缀,它将把终端与进程分离,并将输出和错误信息发送到其默认日志文件,即当前目录中的 nohup.out。你可以像下面的命令一样配置输出文件。
示例:
nohup ./git-p4.py clone --p4_spec p4-specs/file-spec.json --repo /root/code/myapp/testrepo -v --revisions @2020/09/09,now > results.out 2>&1 &
注意:-
-
在上述命令中,我们在最后加上‘&’,以释放当前会话中的终端,这样你就可以继续使用终端进行其他操作,而长时间运行的命令会继续在后台运行。
-
我们将输出/错误重定向到自定义文件,即通过在上述命令中使用这些行“> results.out 2>&1”将结果保存到 results.out。
-
我们通过在命令前加上‘nohup’,启动了‘workflow.py’脚本。
如何在 Linux 上将命令的输出/错误重定向到文件?
文件描述符:
这些是你在终端运行任何命令时会自动打开的文件。
-
0 – 标准输入(STDIN)
-
1 – 标准输出(STDOUT)
-
2 – 标准错误(STDERR)
重定向( > 或 < )操作符:
> – 这个操作符用于将命令的输出重定向到一个文件或设备(其实它也是一个文件)。此外,如果文件已有内容,这个操作符会覆盖文件内容。
< – 这个操作符用于将文件中的输入传递给命令。
>> – 这个操作符将内容追加到文件中。
如何将命令的输出重定向到文件?
ls -l > results.out
如何将命令的输出和错误重定向到文件?
ls -l > results.out 2>&1
#Above command simply telling that first redirect output to a file and then pass the error to output.
如何将命令的输出和错误分别重定向到两个不同的文件?
ls -l > results.out 2> results.err
如何在 bash/终端(Linux)中使用 vi 或 vim 删除文件内容
概述
使用下面的快捷键,可以在 bash/终端中使用 vi 或 vim 删除文件的所有内容
dG
示例
让我们看一个示例
- 首先,让我们创建并写入一个临时文件
echo $'This is \ntest line' > temp.txt
- 让我们打印文件内容进行验证
~ $ cat temp.txt
This is
test line
- 现在运行 vi 或 vim 命令
~ $ vi temp.text
在 vi 编辑器中,你将看到如下内容
This is
test line
~
~
"temp.txt" 2L, 19C
现在按下‘d’,然后按‘G’(使用 shift 键将‘g’转换为大写)。请注意,这些键需要一个接一个地按,而不是一起按。按下键后,内容将被清除。在 vi 或 vim 编辑器中,你将看到如下内容
~
~
--No lines in buffer--
现在保存文件。使用快捷键
ESC key + :wg
现在再次检查文件的内容。它将为空
~ $ cat temp.txt
如何使用 vi 或 vim(Linux)设置文件行号
概述
通常,使用vi或vim打开 Linux 文件时,不会显示行号。可以使用以下命令在vi或vim编辑器中显示行号:
:set nu + Enter key
示例
以下是设置文件行号的示例:
- 创建一个包含一些内容的临时文件:
~ $ touch temp.txt
~ $ echo $'this is line 1\nthis is line 2\nthis is line 3\nthis is line 4' > temp.txt
- 使用cat命令检查临时文件的内容:
~ $ cat temp.txt
this is line 1
this is line 2
this is line 3
this is line 4
- 现在使用vi或vim打开临时文件,然后输入“:set nu”,按下Enter键。这将在打开的编辑器中显示行号,如下所示:
1 this is line 1
2 this is line 2
3 this is line 3
4 this is line 4
~
~
~
~
:set nu
用户可以使用上述方法跳转到所需的行号。操作完成后,可以使用“:q!”(退出文件而不保存更改)或“:wq”(保存更改后退出文件)来关闭文件。
如何在 Linux 中跳转到文件的最后一行
概述
当处理非常大的文件时,例如查看日志文件,用户可能希望跳转到文件的最后一行以查看最新的条目。可以使用以下命令跳转到用 vi 或 vim 命令打开的文件的最后一行:
示例
- 创建一个包含一些内容的临时文件:
# touch temp.txt
# echo $'this is line 1\nthis is line 2\nthis is line 3\nthis is line 4\nThis is last line' > temp.txt
- 使用
cat命令查看临时文件的内容:
# cat temp.txt
this is line 1
this is line 2
this is line 3
this is line 4
This is last line
- 用
vi或vim打开临时文件,然后输入 “😒” 跳转到文件的最后一行。注意,光标已经跳到最后一行:
this is line 1
this is line 2
this is line 3
this is line 4
This is last line
~
~
~
~
:$
清除 Mac 上非滚动终端的屏幕
概述
以下快捷键可用于清除 Mac 上的终端屏幕
cmd + k
执行上述快捷键后,终端屏幕将被清除。此外,屏幕将无法滚动,且无法恢复之前的内容
示例
假设下面是屏幕的当前状态
/ $ pwd
/
/ $
现在按下
cmd + k
屏幕将被清除
/ $
尝试滚动。你将无法滚动到之前的命令。按下上述快捷键后,‘pwd’ 命令将不再显示。
如何在使用 vi 或 vim(Linux)编辑器时通过行号跳转到特定行
概述
通常,在使用 vi 或 vim 打开 Linux 文件时,文件是没有行号显示的。要跳转到某一行,首先需要显示行号。可以使用以下命令在 vi 或 vim 编辑器中显示行号
:set nu + Enter key
一旦在编辑器中显示了行号,就可以在编辑器中输入以下命令来跳转到特定的行号
:n + Enter key
其中 n 是你想要跳转到的行号。让我们来看一个例子
示例
以下是一个示例,展示如何首先显示行号,然后跳转到特定行:
- 创建一个包含一些内容的临时文件:
go`* Check the content of temp file using **cat** command: ~ $ cat temp.txt this is line 1 this is line 2 this is line 3 this is line 4 go * Now open the temp file with **vi** or **vim** and then type **“:set nu”** and then press **Enter** key. This will show line numbers in the opened editor like below: 1 this is line 1 2 this is line 2 3 this is line 3 4 this is line 4 ~ ~ ~ ~ :set nu go * Now go to a particular line number by using **“:n”** followed by **Enter** key where **n** is the line number you want to go to. Here I have used **“:3”** to go to line number **3**. After typing **“:3”** do not forget to press **Enter** key. You will see in your editor that the cursor is at the 3rd line: 1 this is line 1 2 this is line 2 3 this is line 3 4 this is line 4 ~ ~ ~ ~ :3 ```go Users can use the above method to visit/update/delete the desired line. Once the operation is completed, the file can be closed with “:q!” (to quit the file without saving changes) or “:wq” (to quit the file with saved changes).````
Linux 中软链接和硬链接的区别是什么?
在探讨区别之前,我们应该先简要了解一下“什么是 inode”。
什么是 inode:
inode(索引节点)是基于 Unix 的文件系统中的数据结构,用于存储文件的元数据,以及文件数据如何存储的位置。这包括与文件大小、权限、组、所有权、文件类型、文件大小、链接数等相关的信息。请记住!!inode 并不存储文件名。文件名信息始终存储在目录中,并与其相应的 inode 编号一起保存。
软链接:
软链接也被称为符号链接,它是 Windows 上快捷方式的等效物。如果删除原始文件,符号链接将无法工作。每个符号链接都有不同的 inode。
硬链接:
它是文件的另一个别名,不同于快捷方式。如果删除原始文件,硬链接仍然可以工作。
让我们通过示例和图示来理解区别。
testuser-mbp:~ testuser$ touch test.txt
testuser-mbp:~ testuser$ ls -li test.txt
12935258084 -rw-r--r-- 1 testuser staff 0 Sep 5 20:13 test.txt #inode of the file "test.txt" is 12935258084
testuser-mbp:~ testuser$ ln -s test.txt soft_link #Let's create a soft link.
testuser-mbp:~ testuser$ ls -li soft_link #Let's check inode number of the soft_link.
12935259053 lrwxr-xr-x 1 testuser staff 8 Sep 5 20:16 soft_link -> test.txt #inode of the soft_link is different from the file "test.txt" i.e. 12935259053
testuser-mbp:~ testuser$ ln test.txt hard_link #Let's create a hard link now.
testuser-mbp:~ testuser$ ls -li hard_link #Let's check inode number of the hard_link.
12935258084 -rw-r--r-- 2 testuser staff 0 Sep 5 20:13 hard_link #inode of the hard_link is the same as of the file "test.txt" i.e. 12935258084
图示:

总结区别:
| 软链接 | 硬链接 |
|---|---|
| 它有一个与原始文件不同的 inode。 | 它指向原始文件的 inode。 |
| 可以在文件和目录上创建。 | 只能在文件上创建,不能在目录上创建。 |
| 它相当于 Windows 上的快捷方式。 | 它相当于原始文件的另一个副本,指向文件的相同 inode。 |
| 命令 ln -s 文件名 软链接名 | 命令: ln 文件名 硬链接名 |
| 可以跨不同文件系统使用。 | 不能跨不同文件系统使用。原因是一个文件系统的 inode 不能在不同的文件系统间共享,因为它们的结构不同。 |
| 如果删除原始文件,软链接将无法工作。 | 如果删除原始文件,硬链接仍然可以工作。 |
| 软链接支持绝对路径和相对路径。 | 硬链接仅支持绝对路径。 |
| 软链接的任何更改不会影响原始文件。 | 硬链接的任何更改会影响原始文件。 |
| 它需要额外的空间。 | 它不需要额外的空间。 |
如何在 Linux 上为 tomcat 创建环境变量?
假设 tomcat8 安装在 Linux 系统的“/var/lib/tomcat8/bin/”路径下。我们在一个示例文件setenv.sh中包含了所有环境变量,如下所示。
setenv.sh 的内容:
export variable=value
请按照以下步骤操作。这将导出可以被 tomcat 服务访问的环境变量。
sudo su
cd /var/lib/tomcat8/bin/
cp /User/testuser/setenv.sh .
chmod 777 setenv.sh
sudo service tomcat8 restart
BASH shell 或终端中 “./”、 “.” 和 “source” 的区别(Linux)
让我们在示例test.sh文件的上下文中理解这些概念。
使用 “./”
脚本所做的更改会在脚本结束后被丢弃,因为这会通过分叉一个独立的进程来运行脚本。例子:
./test.sh
使用 “.”
脚本所做的更改会持续到终端保持开启,因为脚本在同一个 bash 进程中执行。例子:
. test.sh
使用 “source”
“.” 和 “.” 是同义词,表示相同的意思。“.” 是 POSIX 标准的一部分。例子:
source test.sh
如何在 bash 或终端(Linux)中取消设置一个变量
unset 命令可用于在终端中取消设置一个变量。
让我们来看一个示例
test=some_value
echo "Value before unset=$test"
unset test
echo "Value after unset=$test"
输出
Value before unset=some_value
Value after unset=
如上面的输出所示,在执行 unset 命令后,变量 test 的值变为空。
如何在 Python 中登录 Perforce(p4 命令行)
原文:
techbyexample.com/how-to-login-in-perforcep4-command-line-in-python/
要在程序中登录 p4,您可以编写类似下面的脚本,它接受用户名和密码。这分为两个步骤进行。
步骤 1:设置环境变量。(在 ~/.bashrc 或 ~/.zshrc 或等效文件中创建条目并使其生效)
export P4USER=myuser
export P4PORT=ssl:my.server.perforce.com:31211
source ~/.bashrc 或 ~/.zshrc
步骤 2:使用‘p4 trust’信任机器并尝试登录
以下代码中有两个函数,一个是用于运行操作系统命令,另一个是用于执行登录。在脚本中,我们首先检查机器与服务器之间是否建立了信任关系?然后进一步尝试登录。
import os
import sys
import subprocess
#Utility function to run the OS commands
def execute_shell_command(commands, raise_if_error=True, capture_output=True):
print('Running Command: %s' % ' '.join(commands))
if capture_output:
p = subprocess.Popen(commands, cwd=None, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
out = p.stdout.read().strip().decode('utf-8')
err = p.stderr.read().strip().decode('utf-8')
else:
p = subprocess.Popen(commands)
p.communicate()
exit_status = p.returncode
print('Output: Exit Status : %s' % exit_status)
if exit_status != 0 and raise_if_error:
if capture_output:
raise Exception('Error in running os command: %s. Exit Code: %d, Stdout: `%s`, Stderr: `%s`' % (' '.join(commands), exit_status, out, err))
else:
raise Exception('Error in running os command: %s. Exit Code: %d' % (' '.join(commands), exit_status))
return exit_status
#Function to perform perforce login
def p4_login(ticket, attempt_login=True):
#First check trust is established between P4 server and the machine.
if ticket is not None:
exitStatus = execute_shell_command(['p4', 'trust', '-y', '-f'], raise_if_error=False)
if exitStatus != 0:
sys.exit('Error: p4 trust has been failed. Please check perforce documentation')
exitStatus = execute_shell_command(['p4', '-P', ticket, 'monitor', 'show'], raise_if_error=False)
if exitStatus != 0:
sys.exit('Error: Ticket may be expired. please check.')
else:
exitStatus = execute_shell_command(['p4', 'login', '-s'], raise_if_error=False)
if exitStatus != 0:
if attempt_login:
print('Please enter your Password:')
execute_shell_command(['p4', 'login'], capture_output=False)
p4_login(ticket, attempt_login=False)
else:
sys.exit('Error: You have not logged into Perforce successfully. Please check perforce documentation')
#Now use it something like this
p4_ticket = os.environ.get('P4_TICKET')
p4_login(p4_ticket)
要使用该脚本,将上面的脚本保存在某个文件中,例如‘p4_login.py’,然后在终端中运行以下命令。
source ~/.bashrc or ~/.zshrc
python3 p4_login.py
如何在 Linux 上检查文件大小和目录大小
原文:
techbyexample.com/find-the-file-size-and-directory-size-on-linux/
1. 使用‘du -sh’命令
语法
du -sh file/directory name
示例
- 查找文件的大小。
[root@no1010211066225 git-dir]# du -sh calculate_files.py
4.0K calculate_files.py
- 查找目录的大小。
[root@mac git-dir]# du -sh someDocs
2.9M someDocs
- 查找字典中所有文件的大小。
[root@mac git-dir]# du -sh scripts/*
4.0K scripts/bastionhost_eips.json
8.0K scripts/create_bastion.rb
2. 使用‘stat’命令
语法
stat filename
示例
[root@nmac git-dir]# stat workflow.py
File: ‘workflow.py’
Size: 43000 Blocks: 88 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1058286 Links: 1
Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Context: unconfined_u:object_r:admin_home_t:s0
Access: 2020-09-23 08:33:54.299246431 +0000
Modify: 2020-09-03 05:42:45.113104679 +0000
Change: 2020-09-03 05:42:45.113104679 +0000
Birth: -
注意: stat 命令不会显示目录的内容大小。其大小保持固定。这并不是目录内容的大小。
3. 使用‘ls -l’命令
语法
ls -l filename
示例
[root@mac git-dir]# ls -l workflow.py
-rwxr-xr-x. 1 root root 43000 Sep 3 05:42 workflow.py
注意: 在上面命令的输出中,43000 是文件的字节大小。
[root@mac git-dir] ls -lh workflow.py
-rwxr-xr-x. 1 root root 42K Sep 3 05:42 workflow.py
注意: 在上面命令的输出中,42k 是文件的千字节大小。这是更友好的输出。
如何查找附加到服务器的磁盘和卷的大小/空间
原文:
techbyexample.com/find-the-size-of-disks-and-volumes-attached-to-the-server/
使用‘df -h’命令
语法
df -h Mount name/Filesystem
示例:
[root@macbpro git-spec]# df -h
filesystem Size Used Avail Use% Mounted on
devtmpfs 7.8G 0 7.8G 0% /dev
tmpfs 7.8G 332K 7.8G 1% /dev/shm
tmpfs 7.8G 49M 7.8G 1% /run
tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
/dev/vda1 99G 53G 42G 57% /
tmpfs 512M 174M 339M 34% /tmp
tmpfs 1.6G 0 1.6G 0% /run/user/0
[root@macbpro git-spec]#
[root@macbpro git-spec]# df -h /
filesystem Size Used Avail Use% Mounted on
/dev/vda1 99G 53G 42G 57% /
[root@macbpro git-spec]# df -h /dev
filesystem Size Used Avail Use% Mounted on
devtmpfs 7.8G 0 7.8G 0% /dev
[root@macbpro git-spec]#
[root@macbpro git-spec]# df -h /dev/vda1
filesystem Size Used Avail Use% Mounted on
/dev/vda1 99G 53G 42G 57% /
[root@macbpro git-spec]#
注意: 对于“文件系统”和“挂载名称”,请分别参考第一个示例中的第一列和最后一列。
如何通过云形成(CloudFormation)来增加 EC2 中 EBS 卷的大小。
1. 将 EBS 附加到 EC2:
有多种方法可以将 EBS 附加到 EC2。你可以通过 AWS 控制台 UI 手动将 EBS(块设备)附加到 EC2。你也可以通过终端中的 AWS CLI 来完成这项操作。
这里,我以 AWS CloudFormation 为例,演示如何创建并将 EBS 附加到 EC2。
我们已经使用 CloudFormation 在 EC2 上附加了一个 EBS(块设备)。请参阅下面的 CloudFormation 代码。我们有一个映射部分。在资源部分,我们只需在 LaunchConfig 资源类型中创建一个 BlockDeviceMappings。
"Mappings":{
"InstanceVolumeSize" : {
"Production" : {
"VolumeSize" : "100" #This is the size of EBS in GB
}
}
"Resources": {
"testLaunchConfig": {
"Type": "AWS::AutoScaling::LaunchConfiguration",
"Properties": {
"BlockDeviceMappings" : [
{
"DeviceName" : "/dev/sda1",
"Ebs" : {
"VolumeSize" : {
"Fn::FindInMap" : [ "InstanceVolumeSize", "Production", "VolumeSize" ]
},
"VolumeType" : "gp2",
"DeleteOnTermination": "true"
}
}
]
}
}
2. 调整卷大小,以便它可以扩展到新的分配大小。
在启动/机器启动时,你可以将以下脚本作为用户数据传递,或者你可以 在启动时运行此脚本。请注意,确保在下面的脚本中保持设备名称与在上面 CloudFormation 脚本中提到的 “/dev/sda1” 相同。假设下面的脚本名为 “resize.sh”。
#!/bin/bash
sudo apt install cloud-guest-utils
sudo growpart /dev/xvda 1
sudo resize2fs /dev/xvda1
如何在 Python 中运行操作系统命令
概述
有很多方法可以实现,但我将讨论两种流行的方法。
-
os.system
-
subprocess.Popen
os.system
它通过调用标准 C 函数 system() 在子 shell 中执行字符串命令。它有一些限制,例如 sys.stdin 等的更改不会反映在执行命令的环境中。它仅返回 退出码。
0 – 退出码为 0 表示命令执行成功。
非 0 – 退出码为 0 以外的值表示命令产生了某些错误。
注意:在 Unix 上,返回值是进程的退出状态,按 wait 方法指定的格式编码。在 Windows 上,返回值由系统 shell 返回。
语法:
os.command(cmd);
示例:运行操作系统命令
import os
p=os.system("ls -l")
print(p)
subprocess 模块
所以当您需要命令的输出时,可以使用 subprocess 模块。它内置了多种方法,如 call、Popen,具有更强大的功能。
subprocess.Popen – 在新进程中执行子程序。
本模块允许您生成新的进程,连接其输入/输出/错误管道,并获取它们的 输出、错误和返回码。
语法:
class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout
=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=Non
e, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
-
args – 程序参数的序列或一个字符串
-
bufsize – 在遇到性能问题时增加缓冲区大小,您可以将 bufsize 设置为 -1 或一个足够大的正值(例如 4096)。
-
preexec_fn – 该对象将在子进程执行之前被调用。(仅限 Unix)
-
close_fds (True/False) – 如果为 True,则在子进程执行之前,关闭所有文件描述符,除了 0、1 和 2。(仅限 Unix)。注意,在 Windows 上,您不能将 close_fds 设置为 true。
-
cwd – 更改子进程的当前工作目录。
-
env – 如果您不想继承当前进程的环境(默认行为),可以使用此参数将环境传递给子进程。
-
universal_newlines (True/False) – 如果为 True,则 stderr 和 stout 将采用通用换行符格式处理。
注意: 传递 shell=True 可能会带来安全风险,并且在运行命令之前需要使用 shell 类型的格式,因此需要小心。
示例 1:读取命令的输出:
import subprocess
cmd="uname"
'''
subprocess.PIPE - Special value that can be used as the
stdin, stdout or stderr argument to Popen and indicates
that a pipe to the standard stream should be opened.
'''
#calling method with OS command
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
output = process.stdout.read().strip()
error = process.stderr.read().strip()
process.communicate()
exit_code = process.returncode
#you can process the output like below
if len(output) > 0:
print('Output : {}'.format(output))
#you can pprocess the error like below.
if len(error) > 0:
print('Error : {}'.format(error))
#you can take decision ased on command succcess or failureofthe command like below
print('Exit Code : {}'.format(exit_code))
if exit_code != 0:
raise Exception('Error executing command: {}. Exit Code: {}, Stdout: `{}`, Stderr: `{}`'.format(
' '.join(command_components), exit_code, output, error)
示例 2:我们可以在列表中传递带有参数的命令
p = subprocess.Popen(["p4", "login", "-s"], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
p.communicate()
if p.returncode != 0:
sys.exit('Error: not logged into Perforce');
示例 3:我们可以直接在字符串中传递命令。
p = subprocess.Popen("git ls-files", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
sys.exit('Error: could not get repo files - %s' % stderr)
如何在 python 中读取文件?
使用 open 方法:
我们使用open方法打开文件,并使用read方法读取文件内容。
注意: 使用with语句时,文件的关闭操作会自动处理,因此不需要显式地关闭文件。
def readAFile(path):
with open(path, 'r') as f: #f is file descriptor
return f.read()
readAFile(path)


浙公网安备 33010602011771号