欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

為什么不建議在go項(xiàng)目中使用init()

 更新時(shí)間:2021年04月12日 08:51:55   作者:機(jī)智的小小帥  
這篇文章主要介紹了為什么不建議在go項(xiàng)目中使用init(),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

前言

goinit函數(shù)給人的感覺(jué)怪怪的,我想不明白聰明的 google團(tuán)隊(duì)為何要設(shè)計(jì)出這么一個(gè)“雞肋“的機(jī)制。實(shí)際編碼中,我主張盡量不要使用init函數(shù)。

首先來(lái)看看 init函數(shù)的作用吧。

init() 介紹

init()與包的初始化順序息息相關(guān),所以先介紹一個(gè)go中包的初始化順序吧。(下面的內(nèi)容部分摘自《The go programinng language》)

大體而言,順序如下:

  • 首先初始化包內(nèi)聲明的變量
  • 之后調(diào)用 init 函數(shù)
  • 最后調(diào)用 main 函數(shù)

變量的初始化順序

變量的初始化順序由他們的依賴關(guān)系決定

應(yīng)該任何強(qiáng)類型語(yǔ)言都是這樣子吧。

例如:

var a = b + c;
var b = f();	// 需要調(diào)用 f() 函數(shù)
var c = 1
func f() int{return c + 1;}

a 依賴 bc;b 依賴 f()f() 依賴 c。因此,他們的初始化順序理所當(dāng)然是 c -> b -> a。

graph TB; b-->a c-->a f-->b c-->b

Ps:其實(shí)在這里可能引申出一個(gè)沒(méi)用的小技巧。當(dāng)你有一個(gè)函數(shù)需要在包被初始化的過(guò)程中被調(diào)用時(shí),你可以把這個(gè)函數(shù)賦值給一個(gè)包級(jí)變量。這樣,當(dāng)包被初始化時(shí)就會(huì)自動(dòng)調(diào)用這個(gè)函數(shù)了,這個(gè)函數(shù)甚至能夠在 init() 之前被調(diào)用!不過(guò)話說(shuō)回來(lái),它既然比 init() 更早被調(diào)用,那它才是真正的 init() 才對(duì);此外你也可以在 init() 中調(diào)用該函數(shù),這樣才更合理一些。

// 笨版
// 函數(shù)必須得有一個(gè)返回值才行
var _ = func() interface{} {
	fmt.Println("hello")
	return nil
}()

func init() {
	fmt.Println("world")
}

func main() {

}
// Output:
// hello
// world
// 更合理的版本
func init() {
	fmt.Println("hello")
	fmt.Println("world")
}

func main() {

}
// Output:
// hello
// world

包內(nèi)變量的初始化順序

一個(gè)包內(nèi)往往有多個(gè) go文件,這么go文件的初始化順序由它們被提交給編譯器的順序決定,順序和這些文件的名字有關(guān)。

init()

主角出場(chǎng)了。先來(lái)看看它的設(shè)計(jì)動(dòng)機(jī)吧:

Each variable declared at package level starts life with the value of its initializer expression, if any, but for some variables, like tables of data,an initializer expression may not be the simplest way to set its initial value.In that case,the init function mechanism may be simpler. 《The go pragramming language P44》

這句話的意思是有的包級(jí)變量沒(méi)辦法用一條簡(jiǎn)單的表達(dá)式來(lái)初始化,這個(gè) 時(shí)候,init機(jī)制就派上用場(chǎng)了。

init() 不能被調(diào)用,也不能被 reference,它們會(huì)在程序啟動(dòng)時(shí)自動(dòng)執(zhí)行。

同一個(gè) go 文件中 init 函數(shù)的調(diào)用順序

一個(gè)包內(nèi),甚至 go 文件內(nèi)可以包含多個(gè) init(),同一個(gè) go 文件中的 init() 調(diào)用順序由他們的聲明順序決定 。

func init() {
	fmt.Print("a")
}
func init() {
	fmt.Print("b")
}
func init() {
	fmt.Print("c")
}
// Output
// abc

同一個(gè)包下面不同 go 文件中 init() 的調(diào)用順序

依舊是由它們的聲明順序決定,同一個(gè)包下面的所有go 文件在編譯時(shí)會(huì)被編譯器合并成一個(gè)“大的go文件“(并不是真正合并,僅僅是效果類似而已)。合并的順序由編譯器決定。

不要把程序是否能夠正常工作寄托在init()能夠按照你期待的順序被調(diào)用上。

不過(guò)話說(shuō)回來(lái),正經(jīng)人誰(shuí)在一個(gè)包里寫很多 init() 呀,而且還把這些 init() 放在不同文件里,更可惡的是每個(gè)文件里還有多個(gè) init()。要是看到這樣的代碼,我立馬:@#$%^&*...balabala...

一個(gè)包里最多寫一個(gè)init()(我甚至覺(jué)得最好連一個(gè) init() 都不要有)

不同包內(nèi) init 函數(shù)的調(diào)用順序

唯獨(dú)這個(gè)順序,我們程序員是絕對(duì)可控的。它們的調(diào)用順序由包之間的依賴關(guān)系決定。假設(shè) a包需要 import b包,b包需要import c包,那么很顯然他們的調(diào)用順序是,c包的init()最先被調(diào)用,其次是b包,最后是a包。

graph LRc-->bb-->a

一個(gè)包的init函數(shù)最多會(huì)被調(diào)用一次

道理類似于一個(gè)變量最多會(huì)被初始化一次。

有的同學(xué)會(huì)問(wèn),一個(gè)變量明明可以多次賦值呀,可第二次對(duì)這個(gè)變量賦值那還能夠叫初始化么?

例如有如下的包結(jié)構(gòu),B包和C包都分別import A包,D包需要import B包和C包。

graph TD; A-->B A-->C B-->D C-->D

A包中有 init()

func init() {
	fmt.Println("hello world")
}

D包是 main 包,最終程序只輸出了一句 hello world。

我不喜歡 init 函數(shù)的原因

我不喜歡 init 函數(shù)的一個(gè)重要原因是,它會(huì)隱藏掉程序的一些細(xì)節(jié),它會(huì)在沒(méi)有經(jīng)過(guò)你同意的情況下,偷偷干一些事情。go 的函數(shù)王國(guó)里,所有的函數(shù)都需要程序員顯示的調(diào)用(Call)才會(huì)被執(zhí)行,只有它——init(),是個(gè)例如,你明明沒(méi) Call 它,它卻偷偷執(zhí)行了。

有的同學(xué)會(huì)說(shuō),c++ 里類的構(gòu)造函數(shù)也是在對(duì)象被創(chuàng)建時(shí)就會(huì)默默執(zhí)行呀。確實(shí)是這樣,但在 c++ 里,當(dāng)你點(diǎn)進(jìn)這個(gè)類的定義時(shí),你就能立馬看到它的構(gòu)造函數(shù)和析構(gòu)函數(shù)。在 go 里,當(dāng)你點(diǎn)進(jìn)某個(gè)包時(shí),你能立馬看到包內(nèi)的init()么?這個(gè)包有沒(méi)有init()以及有幾個(gè)init()完全是個(gè)未知數(shù),你需要在包內(nèi)的所有文件中搜索 init() 這個(gè)關(guān)鍵字才能摸清包的 init()情況,而大多數(shù)人包括我懶得費(fèi)這個(gè)功夫。在c++中創(chuàng)建對(duì)象時(shí),程序員能夠很清楚的意識(shí)到這個(gè)操作會(huì)觸發(fā)這個(gè)類的構(gòu)造函數(shù),這個(gè)構(gòu)造函數(shù)的內(nèi)容也能很快找到;但在 go 中,import 包時(shí),一切卻沒(méi)那么清晰了。

希望將來(lái) goland 或者 vscode 能夠分析包內(nèi)的 init() 情況,這樣我對(duì) init() 的惡意會(huì)減半。

init() 給項(xiàng)目維護(hù)帶來(lái)的困難

當(dāng)你看到這樣的 import 代碼時(shí)

import(
	_ "pkg"
)

你立馬能夠知道,這個(gè) import 的目的就是調(diào)用 pkg 包的 int()

當(dāng)看到

import(
	"pkg"
)

你卻很難知道,pkg 包里藏著一個(gè) init(),它被偷偷調(diào)用了。

但這還好,你起碼知道如果 pkg 包有 init() 的話,它會(huì)在此處被調(diào)用。

但當(dāng)pkg 包,被多個(gè)包 import 時(shí),pkg 包內(nèi)的 init() 何時(shí)被調(diào)用的,就是一個(gè)謎了。你得搞清楚這些包之間的 import 先后順序關(guān)系,這是一場(chǎng)噩夢(mèng)。

使用 init()的時(shí)機(jī)

先說(shuō)一下我的結(jié)論:我認(rèn)為 init()應(yīng)該僅被用來(lái)初始化包內(nèi)變量。

《The go programming language》提供了一個(gè)使用 init函數(shù)的例子。

// pc[i] 是 i 中 bit = 1 的數(shù)量
var pc [256]byte

func init() {
	for i := range pc {
		pc[i] = pc[i/2] + byte(i&1)
	}
}

// 返回 x 中等于 1 的 bit 的數(shù)量
func PopCount(x uint64) int {
	return int(pc[byte(x>>(0*8))] +
		pc[byte(x>>(1*8))] +
		pc[byte(x>>(2*8))] +
		pc[byte(x>>(3*8))] +
		pc[byte(x>>(4*8))] +
		pc[byte(x>>(5*8))] +
		pc[byte(x>>(6*8))] +
		pc[byte(x>>(7*8))])
}

PopCount 函數(shù)的作用數(shù)計(jì)算數(shù)字中等于 1bit 的數(shù)量。例如 :

var i uint64 = 2

變量 i 的二進(jìn)制表示形式為

0000000000000000000000000000000000000000000000000000000000000010

把它傳入 PopCount 最終得到的結(jié)果將為 1,因?yàn)樗挥幸粋€(gè) bit 的值為 1。

pc 是一個(gè)表,它的 index 為 x,其中 0 <= x <= 255,value 為 x 中等于 1 的 bit 的數(shù)量。

它的初始化思想是:

  • 如果一個(gè)數(shù)x最后的 bit 為 1,那么這個(gè)數(shù)值為 1 的bit數(shù) = x/2 的值為1的bit數(shù) + 1;
  • 如果一個(gè)數(shù)x最后的 bit 為 0,那么這個(gè)數(shù)值為 1 的bit數(shù) = x/2 的值為1的bit數(shù);

PopCount 中把一個(gè) 8byte 數(shù)拆成了 8 個(gè)單 byte 數(shù),分別計(jì)算這8個(gè)單 byte 數(shù)中 bit1 的數(shù)量,最后累加即可。

這里 pc 的初始化確實(shí)比較復(fù)雜,無(wú)法直接用

var pc = []byte{0, 1, 1,...}

這種形式給出。

一個(gè)可以替代 init()的方法是:

var pc = generatePc()

func generatePc() [256]byte {
	var localPc [256]byte
	for i := range localPc {
		localPc[i] = localPc[i/2] + byte(i&1)
	}
	return localPc
}

我覺(jué)得這樣子初始化比利用 init() 初始化要更好,因?yàn)槟憧梢粤ⅠR知道 pc 是怎樣得來(lái)的,而利用 init() 時(shí),你需要利用 ide 來(lái)查找 pc 的 write reference,之后才能知道,哦,原來(lái)它(pc)來(lái)這里(init()) 被初始化了呀。

當(dāng)包內(nèi)有多個(gè)變量的初始化流程比較復(fù)雜時(shí),可能會(huì)寫出如下代碼。

var pc = generatePc()
var pc2 = generatePc2()
var pc3 = generatePc3()
// ...

有的同學(xué)可能不太喜歡這種寫法,那么用上 init() 后,會(huì)寫成這樣

func init() {
	initPc()
	initPc2()
	initPc3()
}

我覺(jué)得兩種寫法都說(shuō)的過(guò)去吧,雖然我個(gè)人更傾向第一種寫法。

使用 init()的時(shí)機(jī),僅僅有一個(gè)例外,后面說(shuō)。

不使用 init 函數(shù)的時(shí)機(jī)

init()除了初始化變量,不應(yīng)該干其他任何事!

有兩個(gè)原則:

  • 一個(gè)包的 init() 不應(yīng)該依賴包外的環(huán)境
  • 一個(gè)包的 init() 不應(yīng)該對(duì)包外的環(huán)境造成影響

設(shè)置這兩個(gè)原則的理由是:任何對(duì)外部有依賴或者對(duì)外部有影響的代碼都有義務(wù)顯式的讓程序員知曉,不應(yīng)該自己悄咪咪地去做,最好是顯式地讓程序員自己去調(diào)用。

init() 的活動(dòng)范圍就應(yīng)該僅僅被局限在包內(nèi),自己和自己玩,不要影響了其他小朋友的游戲體驗(yàn)。

如下幾條行為就踩了紅線:

  • 讀取配置(依賴于外部的配置文件,且一般讀取配置得到的 obj 會(huì)被其他包訪問(wèn),違反了第一條和第二條)
  • 注冊(cè)路由(因?yàn)樾薷牧?http 包中的 routeMap,會(huì)對(duì) http 包造成影響,違反了第二條)
  • 連接數(shù)據(jù)庫(kù)(連接數(shù)據(jù)庫(kù)后一般會(huì)得到一個(gè) db 對(duì)象給業(yè)務(wù)層去curd吧?違反了第二條)
  • etc... 我暫時(shí)只能想到這么多了

一個(gè)反面教材 https://github.com/go-sql-driver/mysql

反面教材就是:https://github.com/go-sql-driver/mysql 這個(gè)大名鼎鼎的包

當(dāng)使用這個(gè)包時(shí),一個(gè)必不可少的語(yǔ)句是:

import (
	_ "github.com/go-sql-driver/mysql"
)

原因是它里面有個(gè) init函數(shù),會(huì)把自己注冊(cè)到 sql 包里。

func init() {
	sql.Register("mysql", &MySQLDriver{})
}

按照之前的標(biāo)準(zhǔn),此處明顯不符合規(guī)范,因?yàn)樗绊懥藰?biāo)準(zhǔn)庫(kù)的 sql 包。

我認(rèn)為一個(gè)更好的方法是,創(chuàng)建一個(gè) export 的專門用來(lái)做初始化工作的方法:

// Package mysql
func Init() {
	sql.Register("mysql", &MySQLDriver{})
}

然后在 main 包中顯式的調(diào)用它:

// Package main
func main(){
    mysql.Init();
    // other logic
}

來(lái)比較一下兩種方式吧。

  1. 使用 Init()
  • 是否需要告訴開(kāi)發(fā)者額外的信息?

需要。

需要告訴開(kāi)發(fā)者:使用這個(gè)庫(kù)時(shí),記得一定要調(diào)用 Init() 哦,我在里面做了一些工作。

開(kāi)發(fā)者,點(diǎn)進(jìn) Init(),瞬間了然。

  • 是否能夠阻止開(kāi)發(fā)者不正確的調(diào)用?

不能。

因?yàn)槭?export 的,所以開(kāi)發(fā)者可以想到哪兒調(diào)用就到哪兒調(diào)用,想調(diào)用多少次就調(diào)用多少次。

因此需要額外告訴開(kāi)發(fā)者:請(qǐng)您務(wù)必只調(diào)用一次,之后就不要調(diào)用了。且必須在用到 sql 包之前調(diào)用,一般而言都是在 main() 的第一句調(diào)用。

  1. 使用 init()
  • 是否需要告訴開(kāi)發(fā)者額外的信息?

需要

依舊需要告訴開(kāi)發(fā)者,一定要用 _ "github.com/go-sql-driver/mysql"這個(gè)語(yǔ)句顯式的導(dǎo)入包哦,因?yàn)槲依?code>init()在里面做一些工作。

開(kāi)發(fā)者:那你做了什么工作

庫(kù):親,請(qǐng)您點(diǎn)進(jìn) mysql 包,在目錄下搜索 init() 關(guān)鍵字,慢慢找哦。

開(kāi)發(fā)者:......

  • 是否能夠阻止開(kāi)發(fā)者不正確的調(diào)用?

勉強(qiáng)可以吧。

因?yàn)?init() 只會(huì)被調(diào)用一次,不可能被調(diào)用多次,這從根本上杜絕了開(kāi)發(fā)者調(diào)用多次的可能性。

可你管不了開(kāi)發(fā)者的 import 時(shí)機(jī),假設(shè)開(kāi)發(fā)者在其他地方 import 了,導(dǎo)致你在 sql.Open()時(shí),mysqldriver 沒(méi)有被正常注冊(cè),你還是拿開(kāi)發(fā)者沒(méi)有辦法。只能哀嘆一聲:我累了,毀滅吧。

我覺(jué)得作為庫(kù)的提供者,最主要的是提供完善的機(jī)制,在用戶使用你的庫(kù)時(shí),能利用你提供的機(jī)制,寫出無(wú)bug 的代碼。而不是像保姆一樣,想方設(shè)法避免用戶出錯(cuò)。

所以可能使用 init() 為了的優(yōu)勢(shì)就是減少了代碼量吧。

使用 Init() 時(shí),需要兩句代碼

import (
	"github.com/go-sql-driver/mysql"	// 這句
)

func main(){
    mysql.Init()				  // 這句
}

但是使用 init 時(shí),卻只需要一句代碼

import (
	_ "github.com/go-sql-driver/mysql"	// 這句
)

oh yeah,足足少寫了一句代碼!

一個(gè)例外 單元測(cè)試

可能使用 init 的唯一例外就是寫單元測(cè)試的時(shí)候了吧。

假設(shè)我現(xiàn)在需要需要對(duì) dao 層的增刪改查邏輯的寫一個(gè)單元測(cè)試。

func TestCURDPlayer(t *testing.T) {
	// 測(cè)試 curd 玩家信息
}

func TestCURDStore(t *testing.T) {
	// 測(cè)試 curd 商店信息
}

func TestCURDMail(t *testing.T) {
	// 測(cè)試 curd 郵件信息
}

很顯然,這些測(cè)試都是依賴數(shù)據(jù)庫(kù)的,因此為了正常的測(cè)試,必須初始化數(shù)據(jù)庫(kù)

func TestCURDPlayer(t *testing.T) {
	// 測(cè)試 curd 玩家信息
    initdb()
    // balabala
}

func TestCURDStore(t *testing.T) {
	// 測(cè)試 curd 商店信息
    initdb()
    // balabala
}

func TestCURDMail(t *testing.T) {
	// 測(cè)試 curd 郵件信息
    initdb()
    // balabala
}

func initdb(){
    // sql.Open()...
}

難道我每次新增一個(gè)單元測(cè)試,都要在單元測(cè)試的代碼中加一個(gè) initdb() 么,這也太麻煩了吧。

這個(gè)時(shí)候 init() 就派上用場(chǎng)了。可以這樣

func TestCURDPlayer(t *testing.T) {
	// 測(cè)試 curd 玩家信息
    // balabala
}

func TestCURDStore(t *testing.T) {
	// 測(cè)試 curd 商店信息
    // balabala
}

func TestCURDMail(t *testing.T) {
	// 測(cè)試 curd 郵件信息
    // balabala
}

func init(){
    initdb()
}

func initdb(){
    // sql.Open()...
}

這樣,當(dāng)對(duì)這個(gè)文件進(jìn)行單元測(cè)試時(shí),可以確保在執(zhí)行每個(gè) TestXXX 函數(shù)時(shí),db 肯定是被正確初始化了的。

那為什么這個(gè)地方可以利用 init() 來(lái)初始化數(shù)據(jù)庫(kù)呢?

理由之一是它的影響范圍很小,僅僅在 xxx_test.go 文件中生效,在 go run 時(shí)不會(huì)起作用,在 go test 時(shí)才會(huì)起作用。

理由之二是我懶。。。

總結(jié)

init 更像是一個(gè)語(yǔ)法糖,它會(huì)讓開(kāi)發(fā)者對(duì)代碼的追蹤能力變?nèi)酰阅懿挥镁妥詈貌挥谩?/p>

到此這篇關(guān)于為什么不建議在go項(xiàng)目中使用用init()的文章就介紹到這了,更多相關(guān)go init內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 使用Go初始化Struct的方法詳解

    使用Go初始化Struct的方法詳解

    面向?qū)ο缶幊陶Z(yǔ)言最基礎(chǔ)的概念就是類(class),不過(guò)Go語(yǔ)言并沒(méi)有類的概念,所以使用Go語(yǔ)言開(kāi)發(fā)時(shí),我們一般會(huì)用struct(結(jié)構(gòu)體)來(lái)模擬面向?qū)ο笾械念?下面我們來(lái)介紹幾種創(chuàng)建struct類型變量的方法,需要的朋友可以參考下
    2024-01-01
  • Go語(yǔ)言獲取文件的名稱、前綴、后綴

    Go語(yǔ)言獲取文件的名稱、前綴、后綴

    這篇文章主要介紹了Go語(yǔ)言獲取文件的名稱、前綴、后綴,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-05-05
  • 深入了解Golang為什么需要超時(shí)控制

    深入了解Golang為什么需要超時(shí)控制

    本文將介紹為什么需要超時(shí)控制,然后詳細(xì)介紹Go語(yǔ)言中實(shí)現(xiàn)超時(shí)控制的方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下
    2023-05-05
  • golang程序進(jìn)度條實(shí)現(xiàn)示例詳解

    golang程序進(jìn)度條實(shí)現(xiàn)示例詳解

    這篇文章主要為大家介紹了golang程序?qū)崿F(xiàn)進(jìn)度條示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • 詳解Go?flag實(shí)現(xiàn)二級(jí)子命令的方法

    詳解Go?flag實(shí)現(xiàn)二級(jí)子命令的方法

    這篇文章主要介紹了Go?flag?詳解,實(shí)現(xiàn)二級(jí)子命令,本文就探討一下?Go?語(yǔ)言中如何寫一個(gè)擁有類似特性的命令行程序,需要的朋友可以參考下
    2022-07-07
  • Go語(yǔ)言中常見(jiàn)的文件操作分享

    Go語(yǔ)言中常見(jiàn)的文件操作分享

    文件操作應(yīng)該是應(yīng)用程序里非常常見(jiàn)的一種操作,無(wú)論是哪種應(yīng)用場(chǎng)景,幾乎都離不開(kāi)文件的基本操作。Go語(yǔ)言中提供了三個(gè)不同的包去處理文件,下午就來(lái)說(shuō)說(shuō)它們的具體使用
    2023-01-01
  • go項(xiàng)目打包部署的完整步驟

    go項(xiàng)目打包部署的完整步驟

    之前斷斷續(xù)續(xù)的接觸到項(xiàng)目部署,一直沒(méi)有詳細(xì)的了解部署,于是最近就好好的專研一下項(xiàng)目的部署,下面這篇文章主要給大家介紹了關(guān)于go項(xiàng)目打包部署的相關(guān)資料,需要的朋友可以參考下
    2022-09-09
  • go中實(shí)現(xiàn)字符切片和字符串互轉(zhuǎn)

    go中實(shí)現(xiàn)字符切片和字符串互轉(zhuǎn)

    這篇文章主要為大家詳細(xì)介紹了go語(yǔ)言中如何實(shí)現(xiàn)字符切片和字符串互轉(zhuǎn),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解一下
    2023-11-11
  • go modules中replace使用方法

    go modules中replace使用方法

    這篇文章主要為大家介紹了go modules中replace使用方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • go json編譯原理XJSON實(shí)現(xiàn)四則運(yùn)算

    go json編譯原理XJSON實(shí)現(xiàn)四則運(yùn)算

    這篇文章主要為大家介紹了go json編譯原理XJSON實(shí)現(xiàn)四則運(yùn)算示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07

最新評(píng)論