Go使用內嵌型別實現額外保護——以防盜門示例
Go語言支援使用者自定義型別,而自定義型別中還可以再嵌入其他自定義的型別,被嵌入的型別被稱為“內嵌型別/內部型別”,而嵌入內部型別的就被成為“外部型別”。
通過嵌入型別,與內部型別相關的欄位、方法、識別符號等都會被外部型別所擁有,就像外部型別自己的一樣,這樣就達到了程式碼快捷複用組合的目的。
然而 內嵌型別的存在,也讓 共享pkg包中某個型別值,但禁止對該型別值進行復制(建立新地址、重新指向),僅以共享的方式傳遞 成為可能。 這句總結可以說是十分抽象。下面將防盜門的使用這一常見場景來進行示例講解:
一、需求分析
防盜門的生產和使用場景:
- 廠商可以生產防盜門,交到客戶手中(安裝門)時也會把門的密碼告訴客戶
- 建立的防盜門初始密碼都是不相同的
- 客戶輸入密碼,密碼正確的話可以開啟防盜門
- 客戶修改密碼,輸入新舊密碼,密碼正確的話可以修改防盜門密碼為新密碼
- 客戶關上防盜門
在設計功能之前,需要先思考下:
防盜門的本質是什麼?門。門的話,肯定就能夠“開門”和“關門”,但是如果直接由客戶來操作門,顯然沒有任何的安全性。這裡必須由防盜門來操作門,而客戶只能夠操作防盜門,從而保證了安全。門這一本質也由防盜門包含。
二、功能設計
根據上面的需求,可以整理出以下功能點:
-
對外-防盜門(結構:門指標型別,密碼)
- 對外-建立:返回 防盜門指標變數 和其對應的密碼
- 對外-修改密碼:輸入舊密碼和新密碼,正確的話將“防盜門”的密碼改為新密碼
- 對外-開門:輸入密碼,正確的話呼叫“門”的開門方法
- 對外-關門:此處可以不必實現。因為“門”是“防盜門”的內嵌型別,直接呼叫“門”的“關門”方法即可
-
對內-門 (結構:門狀態)
為防止對door型別值進行復制(建立新地址、重新指向),建立防盜門的方法提供給客戶,但方法內部初始化防盜門,便是實現“防盜門1用的門1,永遠不會被更改使用門2、門3,門1也永遠只有防盜門1使用”。
- 對內-開門:修改門的狀態為“開”
- 對外-關門:修改門的狀態為“關”
由需求上分析可知:“門”這一本質也由“防盜門”包含,那麼程式碼實現上便可將door
門作為SecurityDoor
的內嵌型別。
由於防盜門的修改密碼、開門等操作,都是針對“防盜門”本身的,並不是對其複製物件做處理。所以對應操作方法的接收者都是指標接收者
SecurityDoor
結構的內嵌型別也是傳的*door
如果防盜門是可複製的,那我知道防盜門1的密碼,再複製防盜門2,這樣所有的防盜門密碼都是一樣,那就可以被亂開了。為防止此類情況發生,防盜門的建立方法便將指標型別直接返回給客戶進行處理(也可理解為返回了一個安裝在xxx地方的防盜門,相當於定義了防盜門不可複製。就算你想去複製,拿到的也是xxx地方的防盜門,即防盜門1本身,並不是建立了新的防盜門2,只是給防盜門1起了個別名叫做防盜門2)
三、程式碼實現
程式碼實現的目錄結構如下所示。將“防盜門”和“門”相關功能放在door_factory/door_factory.go
檔案中,並作為包。main.go
檔案為模擬客戶操作防盜門
main.go
檔案:模擬客戶進行對防盜門的相關操作
package main
import (
"door_factory"
"fmt"
)
func main() {
//建立一個防盜門,並獲得到初始密碼
securityDoor, password := door_factory.CreateSecurityDoor()
//防盜門是指標型別,指向的地址都是一樣的
securityDoorCopy := securityDoor
fmt.Printf("securityDoor的地址:%p\n", securityDoor)
fmt.Printf("securityDoorCopy的地址:%p\n", securityDoorCopy)
//輸入防盜門正確密碼開門,並顯示結果
fmt.Println("輸入密碼開啟防盜門:", securityDoor.OpenDoor(password))
//防盜門關門,使用的door的方法,但由於door是SecurityDoor的內嵌型別,其方法也可被SecurityDoor呼叫
fmt.Println("關閉防盜門:", securityDoor.CloseDoor())
//修改原密碼為"1234567890"
newPassword := "1234567890"
msg, success := securityDoor.ChangePassword(password, newPassword)
fmt.Println(msg)
//如果修改密碼成功,再輸入舊密碼,將會登入失敗
if success {
//輸入防盜門錯誤密碼開門,並顯示結果
fmt.Println("輸入舊密碼開門:", securityDoor.OpenDoor(password))
}
/**
結果輸出:
securityDoor的地址:0xc000004660
securityDoorCopy的地址:0xc000004660
輸入密碼開啟防盜門: 門已開啟
關閉防盜門: 門已關閉
更新密碼成功
輸入舊密碼開門: 密碼錯誤
*/
}
door_factory/door_factory.go
檔案:
package door_factory
import (
"bytes"
"crypto/rand"
"fmt"
"math/big"
)
type SecurityDoor struct {
*door //door受保護,僅SecurityDoor可被外部呼叫
password string
}
//建立一個防盜門
func CreateSecurityDoor() (*SecurityDoor, string) {
password := createRandomNumber(10)
securityDoor := SecurityDoor{
door: &door{isOpen: false},
password: password,
}
return &securityDoor, password
}
//更改防盜門的密碼
func (securityDoor *SecurityDoor) ChangePassword(oldPwd string, newPwd string) (string, bool) {
//如果密碼錯誤,返回操作錯誤
if securityDoor.password != oldPwd {
return "密碼錯誤", false
}
securityDoor.password = newPwd
return "更新密碼成功", true
}
//門,外部無法呼叫,僅包內可用,但外部可以獲得當前門是否開啟的狀態
type door struct {
isOpen bool
}
//防盜門開門方法-需要輸密碼,外部呼叫
func (securityDoor *SecurityDoor) OpenDoor(password string) string {
if securityDoor.password != password {
return "密碼錯誤"
}
return securityDoor.door.openDoor()
}
//door開門方法-僅包內可用
func (door *door) openDoor() string {
if door.isOpen {
return "門沒關,可以直接開"
}
door.isOpen = true
return "門已開啟"
}
//door關門方法-外部也可呼叫
func (door *door) CloseDoor() string {
if !door.isOpen {
return "門本來就關著"
}
door.isOpen = false
return "門已關閉"
}
//指定位數隨機數
func createRandomNumber(len int) string {
var numbers = []byte{1, 2, 3, 4, 5, 7, 8, 9}
var container string
length := bytes.NewReader(numbers).Len()
for i := 1; i <= len; i++ {
random, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
if err != nil {
}
container += fmt.Sprintf("%d", numbers[random.Int64()])
}
return container
}
四、總結
-
為了讓客戶能夠間接操作 “門” ,可以建立一個類似“防盜門”的結構體和對應方法,實現對內嵌型別操作的額外保護
-
額外保護藉助了包的公開私有識別符號,如果在同個包,那麼也可對
door
進行宣告、初始化、賦值等操作 -
由於door未公開,在包外無法通過結構字面量初始化該內部型別
-
如果內嵌型別某方法公開,那麼接收者的識別符號提升為外部型別,如上面的"關門"操作,同樣可以訪問。欄位也是同理,還可被用於修改